├── .gitattributes ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml └── intellij-javadocs-4.0.1.xml ├── README.md ├── app ├── .gitignore ├── build.gradle └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── itextpdf │ │ └── android │ │ └── app │ │ └── ui │ │ └── MainActivityTest.kt │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── sample_1.pdf │ ├── sample_2.pdf │ ├── sample_3.pdf │ └── sample_4.pdf │ ├── java │ └── com │ │ └── itextpdf │ │ └── android │ │ └── app │ │ ├── extensions │ │ └── AppCompatActivity+Extension.kt │ │ └── ui │ │ ├── MainActivity.kt │ │ ├── PdfSplitActivity.kt │ │ ├── PdfViewerActivity.kt │ │ └── ShareUtil.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── ic_baseline_folder_open_24.xml │ ├── ic_launcher_background.xml │ └── ic_open.xml │ ├── layout │ ├── activity_main.xml │ ├── activity_pdf_viewer.xml │ ├── activity_split_pdf.xml │ └── recycler_item_pdf_selection.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── 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-night │ └── themes.xml │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ └── provider_paths.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── library ├── .gitignore ├── build.gradle └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── itextpdf │ │ └── android │ │ └── library │ │ ├── ContextExtensionInstrumentedTest.kt │ │ ├── fragments │ │ ├── PdfActivityTest.kt │ │ ├── PdfFragmentTest.kt │ │ ├── SplitDocumentFragmentTest.kt │ │ └── SplitPdfActivityTest.kt │ │ ├── helpers │ │ ├── CustomViewActions.kt │ │ └── RecyclerViewMatcher.kt │ │ └── util │ │ └── PdfManipulatorImplTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── itextpdf │ │ │ └── android │ │ │ └── library │ │ │ ├── Constants.kt │ │ │ ├── PdfActivity.kt │ │ │ ├── SplitPdfActivity.kt │ │ │ ├── annotations │ │ │ └── AnnotationAction.kt │ │ │ ├── extensions │ │ │ ├── Context+Extension.kt │ │ │ ├── DeviceRgb+Extension.kt │ │ │ ├── PdfAnnotation+Extension.kt │ │ │ ├── PdfDocument+Extensions.kt │ │ │ ├── PdfPage+Extension.kt │ │ │ ├── PdfView+Extension.kt │ │ │ ├── RectF+Extension.kt │ │ │ └── TypedArray+Extensions.kt │ │ │ ├── fragments │ │ │ ├── PdfConfig.kt │ │ │ ├── PdfFragment.kt │ │ │ ├── PdfResult.kt │ │ │ └── SplitDocumentFragment.kt │ │ │ ├── lists │ │ │ ├── PdfAdapter.kt │ │ │ ├── PdfViewHolder.kt │ │ │ ├── annotations │ │ │ │ ├── AnnotationsAdapter.kt │ │ │ │ └── AnnotationsViewHolder.kt │ │ │ ├── highlighting │ │ │ │ ├── HighlightColorAdapter.kt │ │ │ │ ├── HighlightColorViewHolder.kt │ │ │ │ └── HighlightingPreview.kt │ │ │ ├── navigation │ │ │ │ └── PdfNavigationViewHolder.kt │ │ │ └── split │ │ │ │ └── PdfSplitViewHolder.kt │ │ │ ├── paging │ │ │ ├── Page.kt │ │ │ └── PaginationScrollListener.kt │ │ │ ├── util │ │ │ ├── DisplayUtil.kt │ │ │ ├── FileUtil.kt │ │ │ ├── FileUtilImpl.kt │ │ │ ├── ImageUtil.kt │ │ │ ├── PdfManipulator.kt │ │ │ ├── PdfManipulatorImpl.kt │ │ │ ├── PointPositionMappingInfo.kt │ │ │ └── RectanglePositionMappingInfo.kt │ │ │ └── views │ │ │ ├── PdfThumbnailView.kt │ │ │ └── PdfViewScrollHandle.kt │ └── res │ │ ├── drawable-xhdpi │ │ ├── background_rounded_light_orange.xml │ │ ├── background_slightly_rounded_light_orange.xml │ │ ├── border_background_grey.xml │ │ └── border_background_orange.xml │ │ ├── drawable │ │ ├── circle_shape_with_border.xml │ │ ├── ic_annotation.xml │ │ ├── ic_arrow_left.xml │ │ ├── ic_arrow_right.xml │ │ ├── ic_check.xml │ │ ├── ic_close.xml │ │ ├── ic_delete.xml │ │ ├── ic_edit.xml │ │ ├── ic_help_outline.xml │ │ ├── ic_highlight.xml │ │ ├── ic_more_horizontal.xml │ │ ├── ic_navigate.xml │ │ ├── ic_send.xml │ │ ├── ic_speech_bubble.xml │ │ ├── ic_speech_bubble_old.xml │ │ ├── ic_split.xml │ │ ├── navigation_page_background.xml │ │ ├── navigation_page_border_background.xml │ │ └── scroll_bar.xml │ │ ├── layout │ │ ├── activity_pdf.xml │ │ ├── activity_split_pdf.xml │ │ ├── bottom_sheet_annotations.xml │ │ ├── bottom_sheet_highlight.xml │ │ ├── bottom_sheet_navigate.xml │ │ ├── custom_scroll_handle.xml │ │ ├── fragment_pdf.xml │ │ ├── fragment_split_document.xml │ │ ├── recycler_item_annotation.xml │ │ ├── recycler_item_highlight_color.xml │ │ ├── recycler_item_navigation_pdf_page.xml │ │ ├── recycler_item_split_pdf_page.xml │ │ └── view_pdf_thumbnail.xml │ │ ├── menu │ │ ├── menu_confirm.xml │ │ ├── menu_pdf_fragment.xml │ │ ├── menu_split_document.xml │ │ └── popup_menu_annotation.xml │ │ ├── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── provider_paths.xml │ └── test │ └── assets │ ├── sample_1.pdf │ ├── sample_2.pdf │ ├── sample_3.pdf │ └── sample_4.pdf └── settings.gradle /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to LF line endings on checkout. 6 | *.afm text eol=lf 7 | *.cmap text eol=lf 8 | *.crt text eol=lf 9 | *.cs text eol=lf 10 | *.html text eol=lf 11 | *.java text eol=lf ident 12 | *.lng text eol=lf 13 | *.md text eol=lf 14 | *.pom text eol=lf 15 | *.properties text eol=lf 16 | *.txt text eol=lf 17 | *.xfdf text eol=lf 18 | *.xml text eol=lf 19 | 20 | # Declare files that will always have CRLF line endings on checkout. 21 | *.bat text eol=crlf 22 | *.csproj text eol=crlf 23 | *.sln text eol=crlf 24 | 25 | # Denote all files that are truly binary and should not be modified. 26 | *.bmp binary 27 | *.cmp binary 28 | *.dib binary 29 | *.gif binary 30 | *.j2k binary 31 | *.jb2 binary 32 | *.jp2 binary 33 | *.jpg binary 34 | *.key binary 35 | *.otf binary 36 | *.pdf binary 37 | *.pfb binary 38 | *.png binary 39 | *.tif binary 40 | *.tiff binary 41 | *.ttc binary 42 | *.ttf binary 43 | *.wmf binary 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | #mac 3 | .DS_Store 4 | 5 | # android stuio 6 | *.apk 7 | *.aab 8 | output-metadata.json 9 | /captures 10 | .externalNativeBuild 11 | .cxx 12 | 13 | # keystores 14 | *.jks 15 | 16 | # Created by https://www.gitignore.io 17 | 18 | ### Java ### 19 | *.class 20 | 21 | # Mobile Tools for Java (J2ME) 22 | .mtj.tmp/ 23 | 24 | # Package Files # 25 | *.jar 26 | *.war 27 | *.ear 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | 32 | 33 | ### Eclipse ### 34 | *.pydevproject 35 | .metadata 36 | .gradle 37 | bin/ 38 | tmp/ 39 | *.tmp 40 | *.bak 41 | *.swp 42 | *~.nib 43 | local.properties 44 | .settings/ 45 | .loadpath 46 | 47 | # Eclipse Core 48 | .project 49 | 50 | # External tool builders 51 | .externalToolBuilders/ 52 | 53 | # Locally stored "Eclipse launch configurations" 54 | *.launch 55 | 56 | # CDT-specific 57 | .cproject 58 | 59 | # JDT-specific (Eclipse Java Development Tools) 60 | .classpath 61 | 62 | # PDT-specific 63 | .buildpath 64 | 65 | # sbteclipse plugin 66 | .target 67 | 68 | # TeXlipse plugin 69 | .texlipse 70 | 71 | 72 | ### Intellij ### 73 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 74 | 75 | *.iml 76 | 77 | ## Directory-based project format: 78 | .idea/ 79 | # if you remove the above rule, at least ignore the following: 80 | 81 | # User-specific stuff: 82 | # .idea/workspace.xml 83 | # .idea/tasks.xml 84 | # .idea/dictionaries 85 | # .idea/misc.xml 86 | 87 | # Sensitive or high-churn files: 88 | # .idea/dataSources.ids 89 | # .idea/dataSources.xml 90 | # .idea/sqlDataSources.xml 91 | # .idea/dynamic.xml 92 | # .idea/uiDesigner.xml 93 | 94 | # Gradle: 95 | # .idea/gradle.xml 96 | # .idea/libraries 97 | 98 | # Mongo Explorer plugin: 99 | # .idea/mongoSettings.xml 100 | 101 | ## File-based project format: 102 | *.ipr 103 | *.iws 104 | 105 | ## Plugin-specific files: 106 | 107 | # IntelliJ 108 | out/ 109 | 110 | # mpeltonen/sbt-idea plugin 111 | .idea_modules/ 112 | 113 | # JIRA plugin 114 | atlassian-ide-plugin.xml 115 | 116 | # Crashlytics plugin (for Android Studio and IntelliJ) 117 | com_crashlytics_export_strings.xml 118 | crashlytics.properties 119 | crashlytics-build.properties 120 | 121 | 122 | ### NetBeans ### 123 | nbproject/private/ 124 | build/ 125 | nbbuild/ 126 | dist/ 127 | nbdist/ 128 | nbactions.xml 129 | nb-configuration.xml 130 | .nb-gradle/ 131 | 132 | 133 | ### Linux ### 134 | *~ 135 | 136 | # KDE directory preferences 137 | .directory 138 | 139 | # Linux trash folder which might appear on any partition or disk 140 | .Trash-* 141 | 142 | 143 | ### Windows ### 144 | # Windows image file caches 145 | Thumbs.db 146 | ehthumbs.db 147 | 148 | # Folder config file 149 | Desktop.ini 150 | 151 | # Recycle Bin used on file shares 152 | $RECYCLE.BIN/ 153 | 154 | # Windows Installer files 155 | *.cab 156 | *.msi 157 | *.msm 158 | *.msp 159 | 160 | # Windows shortcuts 161 | *.lnk 162 | 163 | target/ 164 | nbactions*.xml 165 | .checkstyle 166 | .pmd 167 | .pmdruleset.xml 168 | 169 | 170 | .vagrant/ 171 | 172 | ### Gradle ### 173 | .gradle 174 | /build/ 175 | 176 | # Ignore Gradle GUI config 177 | gradle-app.setting 178 | 179 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 180 | !gradle-wrapper.jar 181 | 182 | # Cache of project 183 | .gradletasknamecache 184 | 185 | ### Gradle Patch ### 186 | **/build/ 187 | gradlew.bat 188 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | itext7-android-ui -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 123 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | 1. [Setup](#setup) 3 | 1. [Toubleshooting](#troubleshooting) 4 | 2. [Using the SDK](#third-example) 5 | 1. [Create PDFFragment via code](#create-pdffragment-via-code) 6 | 2. [Inflate PDFFragment via XML](#inflate-pdffragment-via-xml) 7 | 3. [Split PDF](#split-pdf) 8 | 4. [Manipulate PDF directly (without UI)](#manipulate-pdf-directly-without-ui) 9 | 5. [Receiving results](#receiving-pdf-results) 10 | 11 | # Setup 12 | 13 | The SDK depends on forked versions of [PdfiumAndroid](https://github.com/itext/PdfiumAndroid) and 14 | [AndroidPdfViewer](https://github.com/itext/AndroidPdfViewer) whose artifacts are stored on 15 | [iText Artifactory](https://repo.itextsupport.com/ui/repos/tree/General/android/com/itextpdf/android). Artifacts will 16 | be loaded during the SDK build by gradle. 17 | 18 | If you want to build SDK with custom version of PdfiumAndroid or\and AndroidPdfViewer you need to clone repositories, 19 | make necessary changes and then publish them to local maven storage. So SDK will use custom local artifacts as dependencies. 20 | 21 | ## Troubleshooting 22 | 23 | ### No whitespaces in your paths 24 | Do not use spaces in your local paths/directories, as it can lead to all sorts of errors related to Android NDK, ndk-build or cmake. 25 | 26 | ### NDK setup 27 | If you are having problems related to Android NDK, make sure to follow the correct setup procedure: 28 | https://developer.android.com/studio/projects/install-ndk 29 | 30 | 31 | # Using the SDK 32 | 33 | The SDK provides different Fragments and classes to manipulate PDF files. 34 | 35 | ## Create PDFFragment via code 36 | 37 | You can create a new instance of PdfFragment via code: 38 | 39 | ```kotlin 40 | private fun showPdfFragment(pdfUri: Uri) { 41 | 42 | // See PdfConfig for all available customization options 43 | val config = PdfConfig(pdfUri = pdfUri, showScrollIndicator = true) 44 | 45 | // Create PdfFragment 46 | val pdfFragment = PdfFragment.newInstance(pdfConfig) 47 | 48 | // show fragment, e.g. via supportFragmentManager 49 | 50 | } 51 | ``` 52 | 53 | ## Inflate PDFFragment via XML 54 | 55 | You can also inflate a PDF fragment via XML. In thise case, the fragment is customizable via different styleables, such as app:enable_split_view, etc. 56 | 57 | ```xml 58 | 59 | 60 | 75 | ``` 76 | 77 | ## Split PDF via Fragment 78 | 79 | You can directly launch fragment to split PDF documents via... 80 | 81 | ```kotlin 82 | private fun showSplitFragment(pdfUri: Uri) { 83 | 84 | val config = PdfConfig(pdfUri = pdfUri, showScrollIndicator = true) 85 | val splitFragment = SplitDocumentFragment.newInstance(config) 86 | // show fragment, e.g. via supportFragmentManager 87 | } 88 | ``` 89 | 90 | ## Manipulate PDF directly (without UI) 91 | 92 | You can directly manipulate PDF files without showing a Fragment-UI by using the PDFManipulator: 93 | 94 | ```kotlin 95 | val manipulator = PdfManipulator.create(requireContext(), pdfUri) 96 | 97 | manipulator.addTextAnnotationToPdf(...) 98 | manipulator.splitPdfWithSelection(...) 99 | manipulator.addMarkupAnnotationToPdf(...) 100 | // etc... 101 | ``` 102 | 103 | ## Receiving PDF results 104 | 105 | You can receive fragment results by registering a fragment result listener to your fragmentManager. 106 | 107 | ```kotlin 108 | private fun listenForPdfFragmentResult(fragmentManager: FragmentManager) { 109 | 110 | fragmentManager.setFragmentResultListener(PdfFragment.REQUEST_KEY, this) { requestKey: String, bundle: Bundle -> 111 | 112 | // Retrieve fragment result from bundle 113 | val result: PdfResult? = bundle.getParcelable(PdfFragment.RESULT_FILE) 114 | 115 | when (result) { 116 | is PdfResult.CancelledByUser -> // ... 117 | is PdfResult.PdfEdited -> // ... 118 | is PdfResult.PdfSplit -> // ... 119 | is PdfResult.NoChanges -> // ... 120 | null -> // ... 121 | } 122 | } 123 | } 124 | ``` -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'jacoco' 5 | } 6 | 7 | jacoco { 8 | toolVersion = "0.8.7" 9 | } 10 | 11 | android { 12 | compileSdk 32 13 | 14 | defaultConfig { 15 | applicationId "com.itextpdf.android.app" 16 | minSdk 27 17 | targetSdk 32 18 | versionCode 1 19 | versionName "1.0" 20 | 21 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 22 | 23 | // The following argument makes the Android Test Orchestrator run its 24 | // "pm clear" command after each test invocation. This command ensures 25 | // that the app's state is completely cleared between tests. 26 | testInstrumentationRunnerArguments clearPackageData: 'true' 27 | } 28 | 29 | buildFeatures { 30 | viewBinding true 31 | } 32 | 33 | buildTypes { 34 | release { 35 | minifyEnabled false 36 | } 37 | debug { 38 | testCoverageEnabled = true 39 | } 40 | } 41 | compileOptions { 42 | sourceCompatibility JavaVersion.VERSION_11 43 | targetCompatibility JavaVersion.VERSION_11 44 | } 45 | kotlinOptions { 46 | jvmTarget = '11' 47 | } 48 | } 49 | 50 | dependencies { 51 | // PDF 52 | implementation project(':library') 53 | 54 | implementation 'androidx.core:core-ktx:1.7.0' 55 | implementation 'androidx.appcompat:appcompat:1.4.1' 56 | implementation 'com.google.android.material:material:1.5.0' 57 | implementation 'androidx.constraintlayout:constraintlayout:2.1.3' 58 | 59 | implementation 'org.slf4j:slf4j-api:1.7.36' 60 | 61 | // JUnit 62 | testImplementation 'junit:junit:4.+' 63 | 64 | // Test core-ktx 65 | androidTestImplementation "androidx.test:core-ktx:1.4.0" 66 | 67 | // AndroidJUnitRunner and JUnit Rules 68 | androidTestImplementation "androidx.test:runner:1.4.0" 69 | androidTestImplementation "androidx.test:rules:1.4.0" 70 | 71 | // Assertions 72 | androidTestImplementation "androidx.test.ext:junit-ktx:1.1.3" 73 | androidTestImplementation "androidx.test.ext:truth:1.4.0" 74 | 75 | // Espresso 76 | def espresso_version = "3.5.0-alpha05" 77 | androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version" 78 | androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso_version" 79 | 80 | implementation 'org.jacoco:org.jacoco.agent:0.8.7' 81 | implementation "androidx.core:core-ktx:1.7.0" 82 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 83 | } 84 | 85 | configurations.all{ 86 | resolutionStrategy { 87 | eachDependency { details -> 88 | if ('org.jacoco' == details.requested.group) { 89 | details.useVersion "0.8.7" 90 | } 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/itextpdf/android/app/ui/MainActivityTest.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.app.ui 2 | 3 | 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView.ViewHolder 7 | import androidx.test.espresso.Espresso.onView 8 | import androidx.test.espresso.action.ViewActions.click 9 | import androidx.test.espresso.assertion.ViewAssertions.matches 10 | import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition 11 | import androidx.test.espresso.matcher.ViewMatchers.* 12 | import androidx.test.ext.junit.rules.activityScenarioRule 13 | import androidx.test.ext.junit.runners.AndroidJUnit4 14 | import androidx.test.filters.LargeTest 15 | import androidx.test.platform.app.InstrumentationRegistry 16 | import com.itextpdf.android.app.R 17 | import org.hamcrest.Description 18 | import org.hamcrest.Matcher 19 | import org.hamcrest.Matchers.allOf 20 | import org.hamcrest.TypeSafeMatcher 21 | import org.junit.Rule 22 | import org.junit.Test 23 | import org.junit.runner.RunWith 24 | 25 | @LargeTest 26 | @RunWith(AndroidJUnit4::class) 27 | class MainActivityTest { 28 | 29 | @Rule 30 | @JvmField 31 | var activityScenarioRule = activityScenarioRule() 32 | 33 | @Test 34 | fun mainActivityTest() { 35 | 36 | val context = InstrumentationRegistry.getInstrumentation().targetContext 37 | 38 | // Click on first PDF entry in list 39 | onView(withId(R.id.rvPdfList)) 40 | .perform(actionOnItemAtPosition(0, click())) 41 | 42 | // Click on "annotations" menu-item 43 | onView(allOf(withId(R.id.action_annotations), isDisplayed())) 44 | .perform(click()) 45 | 46 | // Check that no-annotations title is shown 47 | onView(withId(R.id.no_annotations_title)) 48 | .check( 49 | matches( 50 | allOf( 51 | withText(context.getString(R.string.no_annotations_title)), 52 | isDisplayed() 53 | ) 54 | ) 55 | ) 56 | 57 | // Check that no-annotations description is shown 58 | onView(withId(R.id.no_annotations_message)) 59 | .check( 60 | matches( 61 | allOf( 62 | withText(context.getString(R.string.no_annotations_description)), 63 | isDisplayed() 64 | ) 65 | ) 66 | ) 67 | 68 | 69 | // Click navigate-pdf menu item 70 | onView(allOf(withId(R.id.action_navigate_pdf), isDisplayed())) 71 | .perform(click()) 72 | 73 | // Check that pdf-page thumbnails are shown in bottom-sheet 74 | onView(allOf(withId(R.id.rvPdfPages))) 75 | .check(matches(isDisplayed())) 76 | 77 | } 78 | 79 | private fun childAtPosition( 80 | parentMatcher: Matcher, position: Int 81 | ): Matcher { 82 | 83 | return object : TypeSafeMatcher() { 84 | override fun describeTo(description: Description) { 85 | description.appendText("Child at position $position in parent ") 86 | parentMatcher.describeTo(description) 87 | } 88 | 89 | public override fun matchesSafely(view: View): Boolean { 90 | val parent = view.parent 91 | return parent is ViewGroup && parentMatcher.matches(parent) 92 | && view == parent.getChildAt(position) 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 18 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/assets/sample_1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/app/src/main/assets/sample_1.pdf -------------------------------------------------------------------------------- /app/src/main/assets/sample_2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/app/src/main/assets/sample_2.pdf -------------------------------------------------------------------------------- /app/src/main/assets/sample_3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/app/src/main/assets/sample_3.pdf -------------------------------------------------------------------------------- /app/src/main/assets/sample_4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/app/src/main/assets/sample_4.pdf -------------------------------------------------------------------------------- /app/src/main/java/com/itextpdf/android/app/extensions/AppCompatActivity+Extension.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.app.extensions 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.net.Uri 6 | import androidx.activity.result.ActivityResultLauncher 7 | import androidx.activity.result.contract.ActivityResultContracts 8 | import androidx.appcompat.app.AppCompatActivity 9 | import com.itextpdf.android.library.extensions.getFileName 10 | 11 | /** 12 | * An intent that can be used to select a pdf file with the phone's default file explorer. 13 | */ 14 | val AppCompatActivity.selectPdfIntent: Intent 15 | get() { 16 | val intentPDF = Intent(Intent.ACTION_GET_CONTENT) 17 | intentPDF.type = "application/pdf" 18 | intentPDF.addCategory(Intent.CATEGORY_OPENABLE) 19 | return intentPDF 20 | } 21 | 22 | /** 23 | * Registers a request to start an activity for result by calling the AppCompatActivity function registerForActivityResult. 24 | * Returns an ActivityResultLauncher object with the generic type Intent that can be used to launch the selectPdfIntent intent 25 | * to select a pdf file with the phone's default file explorer. 26 | * After the file selection, the callback argument is called which receives the uri to the pdf and the file name of the 27 | * pdf file if the selection was successful. If not, those values can be null, but the callback is always called. 28 | * 29 | * @param callback a callback that is called after selecting a pdf file that returns the uri to the pdf file and the file 30 | * name in case of a successful selection and null when something went wrong. This callback should be 31 | * used to for any desired action with the selected pdf file. 32 | * @return the ActivityResultLauncher to launch the selectPdfIntent 33 | */ 34 | fun AppCompatActivity.registerPdfSelectionResult(callback: (pdfUri: Uri?, fileName: String?) -> Unit): ActivityResultLauncher { 35 | return registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> 36 | if (result.resultCode == Activity.RESULT_OK) { 37 | val data: Intent? = result.data 38 | // Get the Uri of the selected file 39 | val uri: Uri? = data?.data 40 | if (uri != null) { 41 | val fileName = getFileName(uri) 42 | callback(uri, fileName) 43 | } else { 44 | callback(null, null) 45 | } 46 | } else { 47 | callback(null, null) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/com/itextpdf/android/app/ui/PdfSplitActivity.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.app.ui 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import android.widget.Toast 8 | import androidx.appcompat.app.AppCompatActivity 9 | import androidx.core.net.toUri 10 | import com.itextpdf.android.app.R 11 | import com.itextpdf.android.app.databinding.ActivitySplitPdfBinding 12 | import com.itextpdf.android.library.fragments.PdfConfig 13 | import com.itextpdf.android.library.fragments.PdfResult 14 | import com.itextpdf.android.library.fragments.SplitDocumentFragment 15 | 16 | class PdfSplitActivity : AppCompatActivity() { 17 | 18 | private lateinit var binding: ActivitySplitPdfBinding 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | 23 | binding = ActivitySplitPdfBinding.inflate(layoutInflater) 24 | setContentView(binding.root) 25 | 26 | intent?.extras?.let { extras -> 27 | val pdfUriString = extras.getString(EXTRA_PDF_URI) 28 | if (!pdfUriString.isNullOrEmpty()) { 29 | 30 | val pdfUri = Uri.parse(pdfUriString) 31 | val fileName = extras.getString(EXTRA_PDF_TITLE) 32 | val config = PdfConfig(pdfUri = pdfUri, fileName = fileName) 33 | 34 | // set fragment in code 35 | if (savedInstanceState == null) { 36 | val fragment: SplitDocumentFragment = SplitDocumentFragment.newInstance(config) 37 | val fm = supportFragmentManager.beginTransaction() 38 | fm.replace(R.id.pdf_splitter_container, fragment, SPLIT_FRAGMENT_TAG) 39 | fm.commit() 40 | } 41 | } 42 | } 43 | 44 | // listen for the fragment result from the SplitDocumentFragment to get a list pdfUris resulting from the split 45 | supportFragmentManager.setFragmentResultListener(SplitDocumentFragment.SPLIT_DOCUMENT_REQUEST_KEY, this) { requestKey, bundle -> 46 | 47 | val result: PdfResult? = bundle.getParcelable(SplitDocumentFragment.SPLIT_DOCUMENT_RESULT) 48 | handlePdfResult(result) 49 | 50 | supportFragmentManager.clearFragmentResult(requestKey) 51 | 52 | finish() 53 | } 54 | } 55 | 56 | private fun handlePdfResult(result: PdfResult?) { 57 | 58 | when (result) { 59 | is PdfResult.CancelledByUser -> { 60 | result.file.deleteRecursively() 61 | Toast.makeText(this, R.string.cancelled_by_user, Toast.LENGTH_LONG).show() 62 | } 63 | is PdfResult.PdfEdited -> ShareUtil.sharePdf(this, result.file.toUri()) 64 | is PdfResult.PdfSplit -> ShareUtil.sharePdf(this, result.fileContainingSelectedPages) 65 | is PdfResult.NoChanges -> {} // do nothing 66 | null -> Toast.makeText(this, R.string.no_result, Toast.LENGTH_LONG).show() 67 | } 68 | 69 | } 70 | 71 | companion object { 72 | private const val EXTRA_PDF_URI = "EXTRA_PDF_URI" 73 | private const val EXTRA_PDF_TITLE = "EXTRA_PDF_TITLE" 74 | 75 | private const val SPLIT_FRAGMENT_TAG = "splitFragment" 76 | 77 | /** 78 | * Convenience function to launch the PdfViewerActivity. Adds the passed uri and the filename 79 | * as a String extra to the intent and starts the activity. 80 | * 81 | * @param context the context 82 | * @param uri the uri to the pdf file 83 | * @param fileName the name of the pdf file 84 | * @param pdfIndex the index of the pdf file within the list 85 | */ 86 | fun launch(context: Context, uri: Uri, fileName: String?) { 87 | val intent = createIntent(context, uri, fileName) 88 | context.startActivity(intent) 89 | } 90 | 91 | private fun createIntent(context: Context, uri: Uri, fileName: String?): Intent { 92 | val intent = Intent(context, PdfSplitActivity::class.java) 93 | intent.putExtra(EXTRA_PDF_URI, uri.toString()) 94 | intent.putExtra(EXTRA_PDF_TITLE, fileName) 95 | 96 | return intent 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /app/src/main/java/com/itextpdf/android/app/ui/PdfViewerActivity.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.app.ui 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import android.widget.Toast 8 | import androidx.appcompat.app.AppCompatActivity 9 | import androidx.core.net.toUri 10 | import com.itextpdf.android.app.R 11 | import com.itextpdf.android.app.databinding.ActivityPdfViewerBinding 12 | import com.itextpdf.android.library.fragments.PdfConfig 13 | import com.itextpdf.android.library.fragments.PdfFragment 14 | import com.itextpdf.android.library.fragments.PdfResult 15 | 16 | class PdfViewerActivity : AppCompatActivity() { 17 | 18 | private lateinit var binding: ActivityPdfViewerBinding 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | 23 | listenForPdfResults() 24 | 25 | binding = ActivityPdfViewerBinding.inflate(layoutInflater) 26 | setContentView(binding.root) 27 | 28 | intent?.extras?.let { extras -> 29 | val pdfUriString = extras.getString(EXTRA_PDF_URI) 30 | if (!pdfUriString.isNullOrEmpty()) { 31 | 32 | val pdfUri = Uri.parse(pdfUriString) 33 | val fileName = extras.getString(EXTRA_PDF_TITLE) 34 | val pdfIndex = extras.getInt(EXTRA_PDF_INDEX, -1) 35 | 36 | // for pdf with index 0 (Sample 1) use the params set within the xml file, for the other pdfs, replace the fragment 37 | if (pdfIndex != 0) { 38 | // set fragment in code 39 | if (savedInstanceState == null) { 40 | val fragment: PdfFragment 41 | // setup the fragment with different settings based on the index of the selected pdf 42 | when (pdfIndex) { 43 | 1 -> { // Sample 2 44 | 45 | val config = PdfConfig.build { 46 | this.pdfUri = pdfUri 47 | this.fileName = fileName 48 | displayFileName = true 49 | pageSpacing = 100 50 | enableDoubleTapZoom = false 51 | primaryColor = "#295819" 52 | secondaryColor = "#950178" 53 | backgroundColor = "#119191" 54 | helpDialogText = getString(R.string.custom_help_text) 55 | } 56 | 57 | fragment = PdfFragment.newInstance(config) 58 | } 59 | 3 -> { 60 | // Sample 4 61 | 62 | val config = PdfConfig.build { 63 | this.pdfUri = pdfUri 64 | this.fileName = fileName 65 | enableThumbnailNavigationView = false 66 | enableHelpDialog = false 67 | } 68 | 69 | fragment = PdfFragment.newInstance(config) 70 | } 71 | else -> { // Sample 3 and pdfs from file explorer 72 | 73 | val config = PdfConfig.build { 74 | this.pdfUri = pdfUri 75 | this.fileName = fileName 76 | helpDialogTitle = getString(R.string.custom_help_title) 77 | } 78 | 79 | fragment = PdfFragment.newInstance(config) 80 | } 81 | } 82 | 83 | val fm = supportFragmentManager.beginTransaction() 84 | fm.replace(R.id.pdf_fragment_container, fragment, "pdfFragment") 85 | fm.commit() 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | private fun listenForPdfResults() { 93 | 94 | supportFragmentManager.setFragmentResultListener(PdfFragment.REQUEST_KEY, this) { requestKey, bundle -> 95 | val result: PdfResult? = bundle.getParcelable(PdfFragment.RESULT_FILE) 96 | handlePdfResult(result) 97 | supportFragmentManager.clearFragmentResult(requestKey) 98 | finish() 99 | } 100 | 101 | } 102 | 103 | private fun handlePdfResult(result: PdfResult?) { 104 | 105 | when (result) { 106 | is PdfResult.CancelledByUser -> { 107 | result.file.deleteRecursively() 108 | Toast.makeText(this, R.string.cancelled_by_user, Toast.LENGTH_LONG).show() 109 | } 110 | is PdfResult.PdfEdited -> ShareUtil.sharePdf(this, result.file.toUri()) 111 | is PdfResult.PdfSplit -> ShareUtil.sharePdf(this, result.fileContainingSelectedPages) 112 | is PdfResult.NoChanges -> {} // do nothing 113 | null -> Toast.makeText(this, R.string.no_result, Toast.LENGTH_LONG).show() 114 | } 115 | 116 | } 117 | 118 | companion object { 119 | 120 | private const val LOG_TAG = "PdfViewActivity" 121 | 122 | private const val EXTRA_PDF_URI = "EXTRA_PDF_URI" 123 | private const val EXTRA_PDF_TITLE = "EXTRA_PDF_TITLE" 124 | private const val EXTRA_PDF_INDEX = "EXTRA_PDF_INDEX" 125 | 126 | /** 127 | * Convenience function to launch the PdfViewerActivity. Adds the passed uri and the filename 128 | * as a String extra to the intent and starts the activity. 129 | * 130 | * @param context the context 131 | * @param uri the uri to the pdf file 132 | * @param fileName the name of the pdf file 133 | * @param pdfIndex the index of the pdf file within the list 134 | */ 135 | fun launch(context: Context, uri: Uri, fileName: String?, pdfIndex: Int? = null) { 136 | val intent = Intent(context, PdfViewerActivity::class.java) 137 | intent.putExtra(EXTRA_PDF_URI, uri.toString()) 138 | intent.putExtra(EXTRA_PDF_TITLE, fileName) 139 | intent.putExtra(EXTRA_PDF_INDEX, pdfIndex) 140 | context.startActivity(intent) 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /app/src/main/java/com/itextpdf/android/app/ui/ShareUtil.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.app.ui 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import androidx.core.content.FileProvider 8 | import androidx.core.net.toFile 9 | import com.itextpdf.android.app.BuildConfig 10 | import com.itextpdf.android.app.R 11 | 12 | object ShareUtil { 13 | 14 | private fun createPdfShareIntent(context: Context, pdfUri: Uri): Intent { 15 | 16 | val shareableUri = FileProvider.getUriForFile( 17 | context, 18 | BuildConfig.APPLICATION_ID + ".provider", 19 | pdfUri.toFile() 20 | ) 21 | 22 | val shareIntent = Intent(Intent.ACTION_SEND) 23 | shareIntent.putExtra(Intent.EXTRA_STREAM, shareableUri) 24 | shareIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION 25 | shareIntent.type = "application/pdf" 26 | 27 | return shareIntent 28 | } 29 | 30 | /** 31 | * Use this function to open up the share sheet an share one pdf file 32 | * 33 | * @param pdfUri The uri to the pdf that should be shared 34 | */ 35 | fun sharePdf(activity: Activity, pdfUri: Uri) { 36 | 37 | val title = activity.getString(R.string.share_pdf_title) 38 | val shareIntent = createPdfShareIntent(activity, pdfUri) 39 | 40 | activity.startActivity(Intent.createChooser(shareIntent, title)) 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_folder_open_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_open.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 21 | 22 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_pdf_viewer.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_split_pdf.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/recycler_item_pdf_selection.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 18 | 19 | 30 | 31 | 42 | 43 | 56 | 57 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFEFD8 4 | #FFB145 5 | #F79918 6 | #FF9400 7 | #225C85 8 | #00416F 9 | #FF000000 10 | #FFFFFFFF 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | iTextPDF 3 | Open pdf 4 | iText 7 Demo 5 | Filename: %1$s 6 | Share split pdf... 7 | Help: Splitting 8 | Custom help text. 9 | Discarded by user 10 | No result available 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/xml/provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.kotlin_version = '1.6.20' 4 | 5 | repositories { 6 | google() 7 | mavenCentral() 8 | jcenter() 9 | } 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:7.1.3' 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 | 14 | // Required by AndroidPdfViewer 15 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' 16 | classpath 'com.github.dcendents:android-maven-plugin:1.2' 17 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5' 18 | } 19 | } 20 | 21 | allprojects { 22 | repositories { 23 | google() 24 | mavenCentral() 25 | mavenLocal() 26 | 27 | maven { 28 | url "https://repo.itextsupport.com/android" 29 | } 30 | } 31 | } 32 | 33 | 34 | task clean(type: Delete) { 35 | delete rootProject.buildDir 36 | } -------------------------------------------------------------------------------- /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 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Nov 26 10:58:45 CET 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | id 'kotlin-parcelize' 5 | id 'jacoco' 6 | id("org.jetbrains.dokka") version "1.6.10" 7 | } 8 | 9 | jacoco { 10 | toolVersion = "0.8.7" 11 | } 12 | 13 | android { 14 | compileSdk 32 15 | 16 | defaultConfig { 17 | minSdk 27 18 | targetSdk 32 19 | 20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 21 | 22 | // The following argument makes the Android Test Orchestrator run its 23 | // "pm clear" command after each test invocation. This command ensures 24 | // that the app's state is completely cleared between tests. 25 | testInstrumentationRunnerArguments = ['clearPackageData': 'true', 'useTestStorageService': 'true'] 26 | } 27 | 28 | sourceSets { 29 | main { 30 | assets { 31 | srcDirs 'src/test/assets' 32 | } 33 | } 34 | test { 35 | resources { 36 | srcDirs 'src/test/assets' 37 | } 38 | } 39 | } 40 | 41 | buildFeatures { 42 | viewBinding true 43 | } 44 | 45 | buildTypes { 46 | release { 47 | minifyEnabled false 48 | } 49 | debug { 50 | testCoverageEnabled = true 51 | } 52 | innerTest { 53 | matchingFallbacks = ['debug', 'release'] 54 | } 55 | } 56 | compileOptions { 57 | sourceCompatibility JavaVersion.VERSION_11 58 | targetCompatibility JavaVersion.VERSION_11 59 | } 60 | kotlinOptions { 61 | jvmTarget = '11' 62 | } 63 | 64 | testOptions { 65 | 66 | // Enable/Disable android-x test-orchestrator 67 | // The orchestrator is used to ensure empty/clean app-data for each instrumented-test 68 | execution 'ANDROIDX_TEST_ORCHESTRATOR' 69 | 70 | animationsDisabled = true 71 | 72 | unitTests { 73 | includeAndroidResources = true 74 | } 75 | } 76 | } 77 | 78 | dependencies { 79 | // PDF 80 | api("com.itextpdf.android:kernel-android:7.2.2") 81 | api("com.itextpdf.android:layout-android:7.2.2") 82 | api("com.itextpdf.android:io-android:7.2.2") 83 | api("com.itextpdf.android:forms-android:7.2.2") 84 | 85 | implementation 'com.itextpdf.android.pdfviewer:android-pdf-viewer:3.2.0-beta.1' 86 | implementation 'com.itextpdf.android.pdfium:pdfium-android:1.9.0' 87 | 88 | // Android X 89 | implementation 'androidx.core:core-ktx:1.7.0' 90 | implementation 'androidx.appcompat:appcompat:1.4.1' 91 | implementation 'androidx.fragment:fragment-ktx:1.4.1' 92 | implementation "androidx.cardview:cardview:1.0.0" 93 | 94 | // Material design 95 | implementation 'com.google.android.material:material:1.5.0' 96 | androidTestImplementation 'com.google.android.material:material:1.5.0' 97 | 98 | implementation 'androidx.compose.material3:material3:1.0.0-alpha09' 99 | 100 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0' 101 | implementation('org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0') 102 | implementation('androidx.lifecycle:lifecycle-runtime-ktx:2.4.1') 103 | 104 | // Espresso 105 | // Core library 106 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 107 | androidTestImplementation("androidx.test.espresso:espresso-contrib:3.4.0") { 108 | exclude group: 'org.checkerframework', module: 'checker' 109 | } 110 | 111 | // AndroidJUnitRunner and JUnit Rules 112 | androidTestUtil "androidx.test:orchestrator:1.4.2-alpha02" 113 | androidTestUtil "androidx.test.services:test-services:1.4.2-alpha02" 114 | 115 | androidTestImplementation 'androidx.test:core-ktx:1.4.0' 116 | androidTestImplementation 'androidx.test:runner:1.4.0' 117 | androidTestImplementation 'androidx.test:rules:1.4.0' 118 | testImplementation 'junit:junit:4.+' 119 | androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3' 120 | 121 | implementation 'org.jacoco:org.jacoco.agent:0.8.7' 122 | 123 | testImplementation "com.google.truth:truth:1.1.3" 124 | androidTestImplementation "com.google.truth:truth:1.1.3" 125 | 126 | debugImplementation "androidx.fragment:fragment-testing:1.4.1" 127 | implementation "androidx.core:core-ktx:1.7.0" 128 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 129 | } 130 | 131 | 132 | configurations.all { 133 | resolutionStrategy { 134 | eachDependency { details -> 135 | if ('org.jacoco' == details.requested.group) { 136 | details.useVersion "0.8.7" 137 | } 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /library/src/androidTest/java/com/itextpdf/android/library/ContextExtensionInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library 2 | 3 | import android.net.Uri 4 | import androidx.core.content.FileProvider 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import androidx.test.platform.app.InstrumentationRegistry 7 | import com.itextpdf.android.library.extensions.getFileName 8 | import org.junit.Assert.assertEquals 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | import java.io.File 12 | import java.util.* 13 | 14 | /** 15 | * Tests the functions in the Context+Extension 16 | */ 17 | @RunWith(AndroidJUnit4::class) 18 | class ContextExtensionInstrumentedTest { 19 | @Test 20 | fun getFileName() { 21 | // Context of the app under test. 22 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 23 | val expectedFileName = "test.pdf" 24 | 25 | // test with fake path 26 | val testPath = "file://folder1/folder2/folder3/test.pdf" 27 | var fileName = appContext.getFileName(Uri.parse(testPath)) 28 | assertEquals(expectedFileName, fileName) 29 | 30 | // create file in cache directory and use file uri that starts with "file://" 31 | val file = File(appContext.cacheDir, expectedFileName) 32 | try { 33 | file.createNewFile() 34 | } catch (e: Exception) { 35 | e.printStackTrace() 36 | } 37 | fileName = appContext.getFileName(Uri.fromFile(file)) 38 | assertEquals(expectedFileName, fileName) 39 | 40 | // use fileProvider to get file uri that starts with "content://" 41 | val uri = FileProvider.getUriForFile( 42 | Objects.requireNonNull(appContext), 43 | appContext.packageName + ".provider", file 44 | ) 45 | fileName = appContext.getFileName(uri) 46 | assertEquals(expectedFileName, fileName) 47 | } 48 | } -------------------------------------------------------------------------------- /library/src/androidTest/java/com/itextpdf/android/library/fragments/PdfActivityTest.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.fragments 2 | 3 | import androidx.test.espresso.Espresso.onView 4 | import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu 5 | import androidx.test.espresso.action.ViewActions.* 6 | import androidx.test.espresso.matcher.ViewMatchers.* 7 | import androidx.test.ext.junit.rules.activityScenarioRule 8 | import androidx.test.ext.junit.runners.AndroidJUnit4 9 | import androidx.test.filters.LargeTest 10 | import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation 11 | import com.itextpdf.android.library.R 12 | import com.itextpdf.android.library.PdfActivity 13 | import org.junit.Rule 14 | import org.junit.Test 15 | import org.junit.runner.RunWith 16 | 17 | @LargeTest 18 | @RunWith(AndroidJUnit4::class) 19 | internal class PdfActivityTest { 20 | 21 | @Rule 22 | @JvmField 23 | var activityScenarioRule = activityScenarioRule() 24 | 25 | private val context = getInstrumentation().targetContext 26 | 27 | @Test 28 | fun testNavigatePdf() { 29 | onView(withId(R.id.action_navigate_pdf)) 30 | .perform(click()) 31 | } 32 | 33 | @Test 34 | fun testHighlight() { 35 | onView(withId(R.id.action_highlight)) 36 | .perform(click()) 37 | } 38 | 39 | @Test 40 | fun testAnnotations() { 41 | 42 | openActionBarOverflowOrOptionsMenu(context) 43 | onView(withText(context.getString(R.string.split_document))) 44 | .perform(click()) 45 | } 46 | 47 | 48 | @Test 49 | fun testSplitPdf() { 50 | 51 | openActionBarOverflowOrOptionsMenu(context) 52 | onView(withText(context.getString(R.string.annotations))) 53 | .perform(click()) 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /library/src/androidTest/java/com/itextpdf/android/library/fragments/PdfFragmentTest.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.fragments 2 | 3 | import android.net.Uri 4 | import androidx.core.os.bundleOf 5 | import androidx.fragment.app.testing.FragmentScenario 6 | import androidx.fragment.app.testing.launchFragmentInContainer 7 | import androidx.lifecycle.Lifecycle 8 | import androidx.test.espresso.Espresso.onView 9 | import androidx.test.espresso.action.ViewActions.* 10 | import androidx.test.espresso.assertion.ViewAssertions.matches 11 | import androidx.test.espresso.matcher.ViewMatchers.* 12 | import androidx.test.ext.junit.runners.AndroidJUnit4 13 | import androidx.test.filters.LargeTest 14 | import androidx.test.platform.app.InstrumentationRegistry 15 | import com.itextpdf.android.library.R 16 | import com.itextpdf.android.library.util.FileUtil 17 | import org.hamcrest.Matchers.allOf 18 | import org.junit.Test 19 | import org.junit.runner.RunWith 20 | 21 | 22 | @LargeTest 23 | @RunWith(AndroidJUnit4::class) 24 | class PdfFragmentTest { 25 | 26 | private val fileUtil = FileUtil.getInstance() 27 | private val context = InstrumentationRegistry.getInstrumentation().targetContext 28 | 29 | private val file = fileUtil.loadFileFromAssets(context, "sample_1.pdf") 30 | private val pdfUri = Uri.fromFile(file) 31 | private val pdfConfig = PdfConfig(pdfUri = pdfUri) 32 | private val fragmentArgs = bundleOf(PdfFragment.EXTRA_PDF_CONFIG to pdfConfig) 33 | 34 | @Test 35 | fun testRecreation() { 36 | val scenario: FragmentScenario = launchFragmentInContainer( 37 | fragmentArgs = fragmentArgs, 38 | themeResId = R.style.Theme_MaterialComponents_DayNight 39 | ) 40 | 41 | scenario.moveToState(Lifecycle.State.RESUMED) 42 | scenario.recreate() 43 | } 44 | 45 | @Test 46 | fun testLongPress() { 47 | 48 | val scenario: FragmentScenario = launchFragmentInContainer( 49 | fragmentArgs = fragmentArgs, 50 | themeResId = R.style.Theme_MaterialComponents_DayNight 51 | ) 52 | 53 | // Click on "annotations" menu-item 54 | onView(allOf(withId(R.id.pdfView), isDisplayed())) 55 | .perform(longClick()) 56 | 57 | onView(allOf(withId(R.id.etTextAnnotation))) 58 | .check(matches(allOf(isDisplayed()))) 59 | .perform(typeText("Lorem Ipsum")) 60 | 61 | onView(allOf(withId(R.id.btnSaveAnnotation))) 62 | .check(matches(allOf(isDisplayed()))) 63 | .perform(click()) 64 | 65 | onView(allOf(withId(R.id.pdfView), isDisplayed())) 66 | .perform(swipeUp(), click()) 67 | 68 | waitForBottomSheetToOpen() 69 | 70 | onView(withId(R.id.rvAnnotations)) 71 | .perform(click()) 72 | 73 | onView(withId(R.id.ivMore)).perform(click()) 74 | 75 | onView(allOf(withText(context.getString(R.string.edit)))) 76 | .perform(click()) 77 | } 78 | 79 | private fun waitForBottomSheetToOpen() { 80 | 81 | // TODO: We need to wait for the bottom sheet to open and almost all (technically feasible) tasks did not succeed. 82 | // This is why we're currently using the dirty Thread.sleep() which must be removed 83 | // Things I've tried already: 84 | // - Disable windowAnimation-, transitionAnimation- and animatorDuration-scale on emulator: https://developer.android.com/training/testing/espresso/setup 85 | // - Disable animations in build.gradle via testOptions.animationsDisabled = true 86 | Thread.sleep(1000) 87 | 88 | } 89 | 90 | 91 | /** 92 | * The file names of the pdf files that are stored in the assets folder. 93 | */ 94 | private val pdfFileNames = 95 | mutableListOf( 96 | "sample_1.pdf", 97 | "sample_2.pdf", 98 | "sample_3.pdf", 99 | "sample_4.pdf", 100 | "sample_3.pdf", 101 | "sample_2.pdf" 102 | ) 103 | 104 | } -------------------------------------------------------------------------------- /library/src/androidTest/java/com/itextpdf/android/library/fragments/SplitDocumentFragmentTest.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.fragments 2 | 3 | import android.net.Uri 4 | import androidx.core.os.bundleOf 5 | import androidx.fragment.app.testing.FragmentScenario 6 | import androidx.fragment.app.testing.launchFragmentInContainer 7 | import androidx.lifecycle.Lifecycle 8 | import androidx.recyclerview.widget.RecyclerView 9 | import androidx.test.espresso.Espresso.onView 10 | import androidx.test.espresso.action.ViewActions.click 11 | import androidx.test.espresso.contrib.RecyclerViewActions 12 | import androidx.test.espresso.matcher.ViewMatchers.* 13 | import androidx.test.ext.junit.runners.AndroidJUnit4 14 | import androidx.test.filters.LargeTest 15 | import androidx.test.platform.app.InstrumentationRegistry 16 | import com.itextpdf.android.library.R 17 | import com.itextpdf.android.library.util.FileUtil 18 | import org.hamcrest.Matchers.allOf 19 | import org.junit.Test 20 | import org.junit.runner.RunWith 21 | 22 | @LargeTest 23 | @RunWith(AndroidJUnit4::class) 24 | class SplitDocumentFragmentTest { 25 | 26 | private val fileUtil = FileUtil.getInstance() 27 | private val context = InstrumentationRegistry.getInstrumentation().targetContext 28 | 29 | private val file = fileUtil.loadFileFromAssets(context, "sample_1.pdf") 30 | private val pdfUri = Uri.fromFile(file) 31 | private val pdfConfig = PdfConfig(pdfUri = pdfUri) 32 | private val fragmentArgs = bundleOf(SplitDocumentFragment.EXTRA_PDF_CONFIG to pdfConfig) 33 | 34 | @Test 35 | fun testRecreation() { 36 | 37 | val scenario: FragmentScenario = launchFragmentInContainer( 38 | fragmentArgs = fragmentArgs, 39 | themeResId = R.style.Theme_MaterialComponents_DayNight 40 | ) 41 | 42 | scenario.moveToState(Lifecycle.State.RESUMED) 43 | scenario.recreate() 44 | } 45 | 46 | @Test 47 | fun testSelectFirstPageAndSplit() { 48 | 49 | val scenario: FragmentScenario = launchFragmentInContainer( 50 | fragmentArgs = fragmentArgs, 51 | themeResId = R.style.Theme_MaterialComponents_DayNight 52 | ) 53 | 54 | onView(allOf(withId(R.id.rvSplitDocument), isDisplayed())) 55 | .perform(RecyclerViewActions.actionOnItemAtPosition(0, click())) 56 | 57 | onView(allOf(withId(R.id.fabSplit), isDisplayed())) 58 | .perform(click()) 59 | 60 | } 61 | 62 | @Test 63 | fun testCloseButton() { 64 | 65 | val scenario: FragmentScenario = launchFragmentInContainer( 66 | fragmentArgs = fragmentArgs, 67 | themeResId = R.style.Theme_MaterialComponents_DayNight 68 | ) 69 | 70 | onView( 71 | allOf( 72 | withParent(withId(R.id.tbSplitDocumentFragment)), 73 | withContentDescription(R.string.close) 74 | ) 75 | ).perform(click()); 76 | 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /library/src/androidTest/java/com/itextpdf/android/library/fragments/SplitPdfActivityTest.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.fragments 2 | 3 | import androidx.test.espresso.Espresso.onView 4 | import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu 5 | import androidx.test.espresso.action.ViewActions.* 6 | import androidx.test.espresso.assertion.ViewAssertions.matches 7 | import androidx.test.espresso.matcher.ViewMatchers.* 8 | import androidx.test.ext.junit.rules.activityScenarioRule 9 | import androidx.test.ext.junit.runners.AndroidJUnit4 10 | import androidx.test.filters.LargeTest 11 | import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation 12 | import com.itextpdf.android.library.R 13 | import com.itextpdf.android.library.PdfActivity 14 | import com.itextpdf.android.library.SplitPdfActivity 15 | import org.junit.Rule 16 | import org.junit.Test 17 | import org.junit.runner.RunWith 18 | 19 | @LargeTest 20 | @RunWith(AndroidJUnit4::class) 21 | internal class SplitPdfActivityTest { 22 | 23 | @Rule 24 | @JvmField 25 | var activityScenarioRule = activityScenarioRule() 26 | 27 | @Test 28 | fun testHelpDialog() { 29 | onView(withId(R.id.action_split_help)) 30 | .perform(click()) 31 | 32 | onView(withText(R.string.help_dialog_title)) 33 | .check(matches(isDisplayed())) 34 | 35 | onView(withText(R.string.help_dialog_text)) 36 | .check(matches(isDisplayed())) 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /library/src/androidTest/java/com/itextpdf/android/library/helpers/CustomViewActions.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.helpers 2 | 3 | import android.view.View 4 | import androidx.test.espresso.PerformException 5 | import androidx.test.espresso.UiController 6 | import androidx.test.espresso.ViewAction 7 | import androidx.test.espresso.matcher.ViewMatchers.isRoot 8 | import androidx.test.espresso.matcher.ViewMatchers.withId 9 | import androidx.test.espresso.util.HumanReadables 10 | import androidx.test.espresso.util.TreeIterables 11 | import org.hamcrest.Matcher 12 | import java.util.concurrent.TimeoutException 13 | 14 | // CustomViewActions.kt: 15 | /** 16 | * This ViewAction tells espresso to wait till a certain view is found in the view hierarchy. 17 | * @param viewId The id of the view to wait for. 18 | * @param timeout The maximum time which espresso will wait for the view to show up (in milliseconds) 19 | */ 20 | fun waitForView(viewId: Int, timeoutMillis: Long): ViewAction { 21 | return object : ViewAction { 22 | 23 | override fun getConstraints(): Matcher { 24 | return isRoot() 25 | } 26 | 27 | override fun getDescription(): String { 28 | return "wait for a specific view with id $viewId; during $timeoutMillis millis." 29 | } 30 | 31 | override fun perform(uiController: UiController, rootView: View) { 32 | uiController.loopMainThreadUntilIdle() 33 | val startTime = System.currentTimeMillis() 34 | val endTime = startTime + timeoutMillis 35 | val viewMatcher = withId(viewId) 36 | 37 | do { 38 | // Iterate through all views on the screen and see if the view we are looking for is there already 39 | for (child in TreeIterables.breadthFirstViewTraversal(rootView)) { 40 | // found view with required ID 41 | if (viewMatcher.matches(child)) { 42 | return 43 | } 44 | } 45 | // Loops the main thread for a specified period of time. 46 | // Control may not return immediately, instead it'll return after the provided delay has passed and the queue is in an idle state again. 47 | uiController.loopMainThreadForAtLeast(100) 48 | } while (System.currentTimeMillis() < endTime) // in case of a timeout we throw an exception -> test fails 49 | throw PerformException.Builder() 50 | .withCause(TimeoutException()) 51 | .withActionDescription(this.description) 52 | .withViewDescription(HumanReadables.describe(rootView)) 53 | .build() 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /library/src/androidTest/java/com/itextpdf/android/library/helpers/RecyclerViewMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.helpers 2 | 3 | import android.content.res.Resources 4 | import android.content.res.Resources.NotFoundException 5 | import android.view.View 6 | import androidx.recyclerview.widget.RecyclerView 7 | import org.hamcrest.Description 8 | import org.hamcrest.Matcher 9 | import org.hamcrest.TypeSafeMatcher 10 | 11 | fun withRecyclerView(recyclerViewId: Int): RecyclerViewMatcher { 12 | return RecyclerViewMatcher(recyclerViewId) 13 | } 14 | 15 | class RecyclerViewMatcher(val mRecyclerViewId: Int) { 16 | 17 | fun atPosition(position: Int, targetViewId: Int): Matcher { 18 | return atPositionOnView(position, targetViewId) 19 | } 20 | 21 | private fun atPositionOnView(position: Int, targetViewId: Int = -1): Matcher { 22 | 23 | return object : TypeSafeMatcher() { 24 | var resources: Resources? = null 25 | var childView: View? = null 26 | override fun describeTo(description: Description) { 27 | val id = if (targetViewId == -1) mRecyclerViewId else targetViewId 28 | var idDescription = id.toString() 29 | if (resources != null) { 30 | idDescription = try { 31 | resources!!.getResourceName(id) 32 | } catch (var4: NotFoundException) { 33 | String.format("%s (resource name not found)", id) 34 | } 35 | } 36 | description.appendText("with id: $idDescription") 37 | } 38 | 39 | override fun matchesSafely(view: View): Boolean { 40 | resources = view.resources 41 | if (childView == null) { 42 | val recyclerView = view.rootView.findViewById(mRecyclerViewId) as RecyclerView? 43 | childView = if (recyclerView != null) { 44 | recyclerView.findViewHolderForAdapterPosition(position)!!.itemView 45 | } else { 46 | return false 47 | } 48 | } 49 | return if (targetViewId == -1) { 50 | view === childView 51 | } else { 52 | val targetView = childView!!.findViewById(targetViewId) 53 | view === targetView 54 | } 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 11 | 12 | 16 | 17 | 22 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library 2 | 3 | internal object Constants { 4 | const val CONTENT_PREFIX = "content://" 5 | const val FILE_PREFIX = "file://" 6 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/PdfActivity.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import com.itextpdf.android.library.util.FileUtil 6 | 7 | 8 | internal class PdfActivity : AppCompatActivity() { 9 | 10 | private val fileUtil = FileUtil.getInstance() 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | copyFileFromAssets() 14 | super.onCreate(savedInstanceState) 15 | setContentView(R.layout.activity_pdf) 16 | } 17 | 18 | private fun copyFileFromAssets() { 19 | fileUtil.loadFileFromAssets(this, "sample_1.pdf") 20 | } 21 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/SplitPdfActivity.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import com.itextpdf.android.library.fragments.SplitDocumentFragment 6 | import com.itextpdf.android.library.util.FileUtil 7 | 8 | /** 9 | * This class is only used during UI testing regarding XML inflation of [SplitDocumentFragment]. 10 | */ 11 | internal class SplitPdfActivity : AppCompatActivity() { 12 | 13 | private val fileUtil = FileUtil.getInstance() 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | copyFileFromAssets() 17 | super.onCreate(savedInstanceState) 18 | setContentView(R.layout.activity_split_pdf) 19 | } 20 | 21 | private fun copyFileFromAssets() { 22 | fileUtil.loadFileFromAssets(this, "sample_1.pdf") 23 | } 24 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/annotations/AnnotationAction.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.annotations 2 | 3 | import com.itextpdf.kernel.pdf.annot.PdfAnnotation 4 | 5 | internal sealed class AnnotationAction { 6 | object ADD : AnnotationAction() 7 | object HIGHLIGHT : AnnotationAction() 8 | class EDIT(val annotation: PdfAnnotation) : AnnotationAction() 9 | object DELETE : AnnotationAction() 10 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/extensions/Context+Extension.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.extensions 2 | 3 | import android.content.Context 4 | import android.database.Cursor 5 | import android.net.Uri 6 | import android.provider.OpenableColumns 7 | import com.itextpdf.android.library.Constants 8 | import java.io.File 9 | 10 | /** 11 | * Returns the fileName of the file at the given uri. 12 | * 13 | * @param uri the uri of the file we want a fileName for 14 | * @return the fileName if successful, null if not 15 | */ 16 | fun Context.getFileName(uri: Uri): String? { 17 | val uriString: String = uri.toString() 18 | val pdfFile = File(uriString) 19 | var fileName: String? = null 20 | 21 | // get filename 22 | if (uriString.startsWith(Constants.CONTENT_PREFIX)) { 23 | var cursor: Cursor? = null 24 | try { 25 | cursor = 26 | contentResolver.query(uri, null, null, null, null) 27 | if (cursor != null && cursor.moveToFirst()) { 28 | fileName = 29 | cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) 30 | } 31 | } finally { 32 | cursor?.close() 33 | } 34 | } else if (uriString.startsWith(Constants.FILE_PREFIX)) { 35 | fileName = pdfFile.name 36 | } 37 | return fileName 38 | } 39 | -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/extensions/DeviceRgb+Extension.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.extensions 2 | 3 | import com.itextpdf.kernel.colors.DeviceRgb 4 | 5 | internal fun DeviceRgb.getHexString(): String { 6 | return String.format( 7 | "#%02x%02x%02x", 8 | (colorValue[0] * 255).toInt(), 9 | (colorValue[1] * 255).toInt(), 10 | (colorValue[2] * 255).toInt() 11 | ) 12 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/extensions/PdfAnnotation+Extension.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.extensions 2 | 3 | import android.graphics.PointF 4 | import com.itextpdf.kernel.geom.Rectangle 5 | import com.itextpdf.kernel.pdf.annot.PdfAnnotation 6 | 7 | /** 8 | * Checks if one annotation is the same as the other even if the object isn't the same based on 9 | * title, content and rectangle 10 | * 11 | * @param other the other annotation 12 | * @return true if it's the same annotation 13 | */ 14 | internal fun PdfAnnotation.isSameAs(other: PdfAnnotation): Boolean { 15 | 16 | val sameRectangles: Boolean = rectangle.all { other.rectangle.contains(it) } 17 | // val propertiesComparison: Int = compareValuesBy(this, other, 18 | // { it.title.value }, 19 | // { it.contents.value }) 20 | 21 | val propertiesEqual = this.title == other.title && this.contents == other.contents 22 | 23 | return propertiesEqual && sameRectangles 24 | } 25 | 26 | /** 27 | * Checks if an annotation is at a specific position within the pdf coordinate system 28 | * 29 | * @param position 30 | * @return 31 | */ 32 | fun PdfAnnotation.isAtPosition(position: PointF, pageIndex: Int): Boolean { 33 | 34 | if (this.page.getPageIndex() != pageIndex) { 35 | return false 36 | } 37 | 38 | // rectangle should have 4 entries: lower-left and upper-right x and y coordinates: [llx, lly, urx, ury] 39 | if (rectangle.size() < 4) return false 40 | val llx = rectangle.getAsNumber(0).floatValue() 41 | val lly = rectangle.getAsNumber(1).floatValue() 42 | val urx = rectangle.getAsNumber(2).floatValue() 43 | val ury = rectangle.getAsNumber(3).floatValue() 44 | 45 | // check if position is in rectangle bounds 46 | return position.x > llx && position.x < urx && position.y > lly && position.y < ury 47 | } 48 | 49 | /** 50 | * Returns the center point of the annotation within the pdf coordinate system 51 | * 52 | * @return the center point 53 | */ 54 | fun PdfAnnotation.getCenterPoint(): PointF { 55 | // rectangle should have 4 entries: lower-left and upper-right x and y coordinates: [llx, lly, urx, ury] 56 | val llx = rectangle.getAsNumber(0).floatValue() 57 | val lly = rectangle.getAsNumber(1).floatValue() 58 | val urx = rectangle.getAsNumber(2).floatValue() 59 | val ury = rectangle.getAsNumber(3).floatValue() 60 | 61 | val annotationWidth = urx - llx 62 | val annotationHeight = ury - lly 63 | return PointF(llx + annotationWidth / 2, lly + annotationHeight / 2) 64 | } 65 | -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/extensions/PdfDocument+Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.extensions 2 | 3 | import com.itextpdf.kernel.pdf.PdfDocument 4 | import com.itextpdf.kernel.pdf.PdfPage 5 | import com.itextpdf.kernel.pdf.annot.PdfAnnotation 6 | 7 | 8 | /** 9 | * Returns all pages of the given pdf-document. 10 | */ 11 | fun PdfDocument.getPages(): List { 12 | 13 | return buildList { 14 | for (i in 1..numberOfPages) { 15 | add(getPage(i)) 16 | } 17 | } 18 | 19 | } 20 | 21 | /** 22 | * Returns all annotations of the pdf-document. 23 | */ 24 | fun PdfDocument.getAnnotations(): List { 25 | return getPages().flatMap { it.annotations } 26 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/extensions/PdfPage+Extension.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.extensions 2 | 3 | import com.itextpdf.kernel.pdf.PdfPage 4 | 5 | /** 6 | * Returns the zero-based page-index of the pdf-page. 7 | */ 8 | fun PdfPage.getPageIndex(): Int { 9 | return document.getPageNumber(this) - 1 10 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/extensions/PdfView+Extension.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.extensions 2 | 3 | import android.graphics.Point 4 | import android.graphics.PointF 5 | import android.view.MotionEvent 6 | import com.github.barteksc.pdfviewer.PDFView 7 | import com.itextpdf.kernel.geom.Rectangle 8 | import com.shockwave.pdfium.util.SizeF 9 | 10 | /** 11 | * Retrieves the page-index where the [motionEvent] occurred. 12 | * 13 | * @return Zero-based page-index. 14 | */ 15 | internal fun PDFView.getPageIndexForClickPosition(motionEvent: MotionEvent): Int? { 16 | return getPageIndexAtScreenPoint(motionEvent.x, motionEvent.y) 17 | } 18 | 19 | /** 20 | * Retrieves the page-index where from the [x] and [y] point on the screen. 21 | * 22 | * @return Zero-based page-index. 23 | */ 24 | internal fun PDFView.getPageIndexAtScreenPoint(x: Float, y: Float): Int? { 25 | if (pdfFile == null) return null 26 | 27 | val mappedX = -currentXOffset + x 28 | val mappedY = -currentYOffset + y 29 | 30 | return pdfFile.getPageAtOffset(if (isSwipeVertical) mappedY else mappedX, zoom) 31 | } 32 | 33 | /** 34 | * Convert the screen based [motionEvent] to a point in the pdf page coordinate system. 35 | * 36 | * @return PointF in page coordinate system. 37 | */ 38 | internal fun PDFView.convertMotionEventPointToPdfPagePoint(motionEvent: MotionEvent): PointF? { 39 | return convertScreenPointToPdfPagePoint(motionEvent.x, motionEvent.y) 40 | } 41 | 42 | /** 43 | * Convert the screen based [screenRect] to a rectangle in the pdf page coordinate system. The pdf rectangle will also be corrected and cut off if the 44 | * [screenRect] spans over multiple pages. The point that defines on which page the rect will be is the top left of the [screenRect]. 45 | * 46 | * @return The rectangle in pdf page coordinates. 47 | */ 48 | internal fun PDFView.convertScreenRectToPdfPageRect(screenRect: Rectangle): Rectangle? { 49 | // convert lowerLeft point of screenRect to pdfPoint -> Point(x, y+height) 50 | val convertedLowerLeft = convertScreenPointToPdfPagePoint(screenRect.x, screenRect.y + screenRect.height) 51 | // convert upperRight point of screenRect to pdfPoint -> Point (x+width, y) 52 | val convertedUpperRight = convertScreenPointToPdfPagePoint(screenRect.x + screenRect.width, screenRect.y) 53 | return if (convertedLowerLeft != null && convertedUpperRight != null) { 54 | // make sure both points are on the same pdf page -> if not correct the lowerLeft 55 | if (convertedLowerLeft.y > convertedUpperRight.y) { 56 | convertedLowerLeft.y = 0f 57 | } 58 | 59 | val convertedWidth = convertedUpperRight.x - convertedLowerLeft.x 60 | val convertedHeight = convertedUpperRight.y - convertedLowerLeft.y 61 | Rectangle(convertedLowerLeft.x, convertedLowerLeft.y, convertedWidth, convertedHeight) 62 | } else { 63 | null 64 | } 65 | } 66 | 67 | /** 68 | * Convert the screen based [x] and [y] to a point in the pdf page coordinate system. 69 | * 70 | * @return PointF in page coordinate system. 71 | */ 72 | internal fun PDFView.convertScreenPointToPdfPagePoint(x: Float, y: Float): PointF? { 73 | if (pdfFile == null) return null 74 | val mappedX = -currentXOffset + x 75 | val mappedY = -currentYOffset + y 76 | val pageIndex = 77 | pdfFile.getPageAtOffset(if (isSwipeVertical) mappedY else mappedX, zoom) 78 | val pageSize: SizeF = pdfFile.getScaledPageSize(pageIndex, zoom) 79 | val pageX: Int 80 | val pageY: Int 81 | if (isSwipeVertical) { 82 | pageX = pdfFile.getSecondaryPageOffset(pageIndex, zoom).toInt() 83 | pageY = pdfFile.getPageOffset(pageIndex, zoom).toInt() 84 | } else { 85 | pageY = pdfFile.getSecondaryPageOffset(pageIndex, zoom).toInt() 86 | pageX = pdfFile.getPageOffset(pageIndex, zoom).toInt() 87 | } 88 | 89 | return pdfFile.mapDeviceCoordsToPage( 90 | pageIndex, 91 | pageX, 92 | pageY, 93 | pageSize.width.toInt(), 94 | pageSize.height.toInt(), 95 | 0, //TODO: use real rotation 96 | mappedX.toInt(), 97 | mappedY.toInt() 98 | ) 99 | } 100 | 101 | /** 102 | * Convert the pdf page based [pagePoint] to a point on the screen. 103 | * 104 | * @return Point on the screen. 105 | */ 106 | internal fun PDFView.convertPdfPagePointToScreenPoint(pagePoint: PointF, pageIndex: Int): Point? { 107 | val x = pagePoint.x 108 | val y = pagePoint.y 109 | 110 | if (pdfFile == null) return null 111 | 112 | val pageSize = pdfFile.getScaledPageSize(pageIndex, zoom) 113 | val pageSpacing = spacingPx * zoom 114 | 115 | val startX: Int 116 | val startY: Int 117 | if (isSwipeVertical) { 118 | // delta between pdfView width and page width -> has influence on where page actually starts 119 | val widthDelta = (width * zoom) - pageSize.width 120 | // use widthDelta/2 as page is centered and therefore first half of delta is before page, second half after 121 | val widthXOffset = widthDelta / 2 122 | startX = (currentXOffset + widthXOffset).toInt() 123 | startY = (currentYOffset + (pageSize.height + pageSpacing) * pageIndex).toInt() 124 | } else { 125 | // delta between pdfView height and page height -> has influence on where page actually starts 126 | val heightDelta = (height * zoom) - pageSize.height 127 | // use heightDelta/2 as page is centered and therefore first half of delta is before page, second half after 128 | val heightYOffset = heightDelta / 2 129 | startY = (currentYOffset + heightYOffset).toInt() 130 | startX = (currentXOffset + (pageSize.width + pageSpacing) * pageIndex).toInt() 131 | } 132 | 133 | return pdfFile.mapPageCoordsToDevice( 134 | pageIndex, 135 | startX, 136 | startY, 137 | pageSize.width.toInt(), 138 | pageSize.height.toInt(), 139 | 0, //TODO: use real rotation 140 | x.toDouble(), 141 | y.toDouble() 142 | ) 143 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/extensions/RectF+Extension.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.extensions 2 | 3 | import android.graphics.RectF 4 | import com.itextpdf.kernel.geom.Rectangle 5 | import kotlin.math.abs 6 | import kotlin.math.min 7 | 8 | internal fun RectF.toItextRectangle(): Rectangle { 9 | // make sure to use minimum of left/right and bottom/top to always get bottom left corner and also absolute values for width and height 10 | return Rectangle(min(left, right), min(bottom, top), abs(width()), abs(height())) 11 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/extensions/TypedArray+Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.extensions 2 | 3 | import android.content.res.TypedArray 4 | import androidx.annotation.StyleableRes 5 | import androidx.core.content.res.getBooleanOrThrow 6 | import androidx.core.content.res.getIntegerOrThrow 7 | 8 | internal fun TypedArray.getTextIfAvailable(@StyleableRes index: Int, block: (text: CharSequence) -> Unit) { 9 | if (hasValue(index)) { 10 | block.invoke(getText(index)) 11 | } 12 | } 13 | 14 | internal fun TypedArray.getBooleanIfAvailable(@StyleableRes index: Int, block: (value: Boolean) -> Unit) { 15 | if (hasValue(index)) { 16 | block.invoke(getBooleanOrThrow(index)) 17 | } 18 | } 19 | 20 | internal fun TypedArray.getIntegerIfAvailable(@StyleableRes index: Int, block: (value: Int) -> Unit) { 21 | if (hasValue(index)) { 22 | block.invoke(getIntegerOrThrow(index)) 23 | } 24 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/fragments/PdfConfig.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.fragments 2 | 3 | import android.graphics.Color 4 | import android.net.Uri 5 | import android.os.Parcelable 6 | import androidx.annotation.ColorInt 7 | import kotlinx.parcelize.Parcelize 8 | 9 | /** 10 | * The config to be used when manipulating the PDF file. 11 | * 12 | * @param pdfUri The uri of the pdf that should be displayed. This is the only required param 13 | * @param fileName The name of the file that should be displayed 14 | * @param displayFileName A boolean flag that defines if the given file name should be displayed in the toolbar. Default: false 15 | * @param pageSpacing The spacing in px between the pdf pages. Default: 20 16 | * @param enableThumbnailNavigationView A boolean flag to enable/disable pdf thumbnail navigation view. Default: true 17 | * @param enableSplitView A boolean flag to enable/disable pdf split view. Default: true 18 | * @param enableAnnotationRendering A boolean flag to enable/disable annotation rendering. Default: true 19 | * @param enableDoubleTapZoom A boolean flag to enable/disable double tap to zoom. Default: true 20 | * @param showScrollIndicator A boolean flag to enable/disable a scrolling indicator at the right of the page, that can be used fast scrolling. Default: true 21 | * @param showScrollIndicatorPageNumber A boolean flag to enable/disable the page number while the scroll indicator is tabbed. Default: true 22 | * @param primaryColor A color string to set the primary color of the view (affects: scroll indicator, navigation thumbnails and loading indicator). Default: #FF9400 23 | * @param secondaryColor A color string to set the secondary color of the view (affects: scroll indicator and navigation thumbnails). Default: #FFEFD8 24 | * @param backgroundColor A color string to set the background of the pdf view that will be visible between the pages if pageSpacing > 0. Default: #EAEAEA@ 25 | * @param enableHelpDialog A boolean flag to enable/disable the help dialog on the split view 26 | * @param helpDialogTitle The title of the help dialog on the split view. If this is null but help dialog is displayed, a default title is used. 27 | * @param helpDialogText The text of the help dialog on the split view. If this is null but help dialog is displayed, a default text is used. 28 | */ 29 | @Parcelize 30 | data class PdfConfig constructor( 31 | val pdfUri: Uri, 32 | val fileName: String? = FILE_NAME, 33 | val displayFileName: Boolean = DISPLAY_FILE_NAME, 34 | val pageSpacing: Int = PAGE_SPACING, 35 | val enableThumbnailNavigationView: Boolean = ENABLE_THUMBNAIL_NAVIGATION_VIEW, 36 | val enableSplitView: Boolean = ENABLE_SPLITVIEW, 37 | val enableAnnotationRendering: Boolean = ENABLE_ANNOTATION_RENDERING, 38 | val enableDoubleTapZoom: Boolean = ENABLE_DOUBLE_TAP_ZOOM, 39 | val showScrollIndicator: Boolean = SHOW_SCROLL_INDICATOR, 40 | val showScrollIndicatorPageNumber: Boolean = SHOW_SCROLL_INDICATOR_PAGE_NUMBER, 41 | val primaryColor: String = PRIMARY_COLOR, 42 | val secondaryColor: String = SECONDARY_COLOR, 43 | val backgroundColor: String = BACKGROUND_COLOR, 44 | val enableHelpDialog: Boolean = ENABLE_HELP_DIALOG, 45 | val helpDialogTitle: String? = HELP_DIALOG_TITLE, 46 | val helpDialogText: String? = HELP_DIALOG_TEXT, 47 | val enableHighlightView: Boolean = ENABLE_HIGHLIGHT_VIEW, 48 | val enableAnnotationView: Boolean = ENABLE_ANNOTATION_VIEW 49 | ) : Parcelable { 50 | 51 | private constructor(builder: Builder) : this( 52 | pdfUri = builder.pdfUri ?: throw IllegalArgumentException("PDF uri not set. Make sure to specify PDF uri."), 53 | fileName = builder.fileName, 54 | displayFileName = builder.displayFileName, 55 | pageSpacing = builder.pageSpacing, 56 | enableThumbnailNavigationView = builder.enableThumbnailNavigationView, 57 | enableSplitView = builder.enableSplitView, 58 | enableAnnotationRendering = builder.enableAnnotationRendering, 59 | enableDoubleTapZoom = builder.enableDoubleTapZoom, 60 | showScrollIndicator = builder.showScrollIndicator, 61 | showScrollIndicatorPageNumber = builder.showScrollIndicatorPageNumber, 62 | primaryColor = builder.primaryColor, 63 | secondaryColor = builder.secondaryColor, 64 | backgroundColor = builder.backgroundColor, 65 | enableHelpDialog = builder.enableHelpDialog, 66 | helpDialogTitle = builder.helpDialogTitle, 67 | helpDialogText = builder.helpDialogText, 68 | enableHighlightView = builder.enableHighlightView, 69 | enableAnnotationView = builder.enableAnnotationView 70 | ) 71 | 72 | /** 73 | * Returns the corresponding color-int of [primaryColor]. 74 | */ 75 | @ColorInt 76 | fun getPrimaryColorInt(): Int { 77 | return Color.parseColor(primaryColor) 78 | } 79 | 80 | companion object { 81 | 82 | /** 83 | * Builds and returns a new [PdfConfig] with the specified options. 84 | */ 85 | inline fun build(block: Builder.() -> Unit): PdfConfig = Builder().apply(block).build() 86 | 87 | private val FILE_NAME: String? = null 88 | private const val PAGE_SPACING = 10 89 | private const val PRIMARY_COLOR = "#FF9400" 90 | private const val SECONDARY_COLOR = "#FFEFD8" 91 | private const val BACKGROUND_COLOR = "#EAEAEA" 92 | private const val DISPLAY_FILE_NAME = false 93 | private const val ENABLE_THUMBNAIL_NAVIGATION_VIEW = true 94 | private const val ENABLE_SPLITVIEW = true 95 | private const val ENABLE_ANNOTATION_RENDERING = true 96 | private const val ENABLE_DOUBLE_TAP_ZOOM = true 97 | private const val SHOW_SCROLL_INDICATOR = true 98 | private const val SHOW_SCROLL_INDICATOR_PAGE_NUMBER = true 99 | private const val ENABLE_HELP_DIALOG: Boolean = true 100 | private val HELP_DIALOG_TITLE: String? = null 101 | private val HELP_DIALOG_TEXT: String? = null 102 | private const val ENABLE_HIGHLIGHT_VIEW: Boolean = true 103 | private const val ENABLE_ANNOTATION_VIEW: Boolean = true 104 | 105 | } 106 | 107 | /** 108 | * Builder for creating instances of [PdfConfig]. 109 | */ 110 | class Builder { 111 | 112 | var pdfUri: Uri? = null 113 | var fileName: String? = FILE_NAME 114 | var displayFileName: Boolean = DISPLAY_FILE_NAME 115 | var pageSpacing: Int = PAGE_SPACING 116 | var enableThumbnailNavigationView: Boolean = ENABLE_THUMBNAIL_NAVIGATION_VIEW 117 | var enableSplitView: Boolean = ENABLE_SPLITVIEW 118 | var enableAnnotationRendering: Boolean = ENABLE_ANNOTATION_RENDERING 119 | var enableDoubleTapZoom: Boolean = ENABLE_DOUBLE_TAP_ZOOM 120 | var showScrollIndicator: Boolean = SHOW_SCROLL_INDICATOR 121 | var showScrollIndicatorPageNumber: Boolean = SHOW_SCROLL_INDICATOR_PAGE_NUMBER 122 | var primaryColor: String = PRIMARY_COLOR 123 | var secondaryColor: String = SECONDARY_COLOR 124 | var backgroundColor: String = BACKGROUND_COLOR 125 | var enableHelpDialog: Boolean = ENABLE_HELP_DIALOG 126 | var helpDialogTitle: String? = HELP_DIALOG_TITLE 127 | var helpDialogText: String? = HELP_DIALOG_TEXT 128 | var enableHighlightView: Boolean = ENABLE_HIGHLIGHT_VIEW 129 | var enableAnnotationView: Boolean = ENABLE_ANNOTATION_VIEW 130 | 131 | fun build() = PdfConfig(this) 132 | 133 | } 134 | 135 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/fragments/PdfResult.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.fragments 2 | 3 | import android.net.Uri 4 | import android.os.Parcelable 5 | import kotlinx.parcelize.Parcelize 6 | import java.io.File 7 | 8 | /** 9 | * The result that is returned by e.g. [PdfFragment] or [SplitDocumentFragment]. 10 | */ 11 | sealed class PdfResult : Parcelable { 12 | 13 | /** 14 | * Determines that there are no changes. 15 | */ 16 | @Parcelize 17 | object NoChanges : PdfResult() 18 | 19 | /** 20 | * Determines that the user has cancelled the action and the working copy [file] that should be discarded. 21 | * 22 | * @property file The working copy with the current changes that should be discarded. 23 | */ 24 | @Parcelize 25 | class CancelledByUser(val file: File) : PdfResult() 26 | 27 | /** 28 | * Determines that the user has edited the PDF file and the result is stored in the given [file]. 29 | * 30 | * @property file The file where the edited PDF is stored. 31 | */ 32 | @Parcelize 33 | class PdfEdited(val file: File) : PdfResult() 34 | 35 | /** 36 | * The user has split the given PDF file into two parts. 37 | * The user-selected pages have been stored to a new PDF file at [fileContainingSelectedPages]. 38 | * The unselected pages have been stored to a new PDF file at [fileContainingUnselectedPages]. 39 | * 40 | * @property fileContainingSelectedPages File that contains the selected pages. Will contain all pages of the original PDF if user did not select any page. 41 | * @property fileContainingUnselectedPages This will be null if user selected all or no pages. 42 | */ 43 | @Parcelize 44 | class PdfSplit( 45 | val fileContainingSelectedPages: Uri, 46 | val fileContainingUnselectedPages: Uri? 47 | ) : PdfResult() 48 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/lists/PdfAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.lists 2 | 3 | import android.graphics.Color 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import androidx.core.graphics.drawable.DrawableCompat 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.itextpdf.android.library.lists.PdfRecyclerItem.Companion.TYPE_NAVIGATE 9 | import com.itextpdf.android.library.lists.PdfRecyclerItem.Companion.TYPE_SPLIT 10 | import com.itextpdf.android.library.lists.navigation.PdfNavigationViewHolder 11 | import com.itextpdf.android.library.lists.split.PdfSplitViewHolder 12 | 13 | /** 14 | * Adapter class for the pdf thumbnail navigation 15 | * 16 | * @property data a list of page items 17 | * 18 | * @param primaryColorString the primary color that is used for highlighting selected elements. optional 19 | * @param secondaryColorString the secondary color that is used for highlighting selected elements. optional 20 | */ 21 | internal class PdfAdapter( 22 | private val data: List, 23 | private val allowMultiSelection: Boolean, 24 | primaryColorString: String?, 25 | secondaryColorString: String? 26 | ) : 27 | RecyclerView.Adapter() { 28 | private var selectedPositions = mutableListOf() 29 | 30 | private val primaryColor: Int? = if (primaryColorString != null) { 31 | Color.parseColor(primaryColorString) 32 | } else { 33 | null 34 | } 35 | private val secondaryColor: Int? = if (secondaryColorString != null) { 36 | Color.parseColor(secondaryColorString) 37 | } else { 38 | null 39 | } 40 | private val backgroundColorNotSelected: Int = Color.parseColor(BACKGROUND_COLOR_NOT_SELECTED) 41 | private val borderColorNotSelected: Int = Color.parseColor(BORDER_COLOR_NOT_SELECTED) 42 | 43 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PdfViewHolder { 44 | val view = LayoutInflater.from(parent.context).inflate( 45 | viewType, 46 | parent, 47 | false 48 | ) 49 | 50 | return when (viewType) { 51 | TYPE_NAVIGATE -> PdfNavigationViewHolder(view) 52 | TYPE_SPLIT -> PdfSplitViewHolder(view) 53 | else -> throw IllegalStateException("Unsupported viewType $viewType") 54 | } 55 | } 56 | 57 | override fun getItemCount(): Int = data.size 58 | 59 | override fun getItemViewType(position: Int): Int { 60 | return data[position].type 61 | } 62 | 63 | override fun onBindViewHolder(holder: PdfViewHolder, position: Int) { 64 | val item = data[position] 65 | val selected = selectedPositions.contains(position) 66 | 67 | holder.bind(item) 68 | holder.itemView.isSelected = selected 69 | holder.updateTextSize(selected) 70 | 71 | if (primaryColor != null && secondaryColor != null) { 72 | val background = holder.itemView.background 73 | if (selected) { 74 | DrawableCompat.setTint(background, secondaryColor) 75 | holder.updateBorderColor(primaryColor) 76 | } else { 77 | DrawableCompat.setTint(background, backgroundColorNotSelected) 78 | holder.updateBorderColor(borderColorNotSelected) 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * Returns a sorted list of selected positions 85 | * 86 | * @return the sorted list of positions 87 | */ 88 | fun getSelectedPositions(): List { 89 | return selectedPositions.sorted() 90 | } 91 | 92 | /** 93 | * Updates the selection 94 | * 95 | * @param selectedIndex the index of the newly selected item 96 | */ 97 | fun updateSelectedItem(selectedIndex: Int) { 98 | if (allowMultiSelection) { 99 | if (selectedPositions.contains(selectedIndex)) { 100 | selectedPositions.remove(selectedIndex) 101 | } else { 102 | selectedPositions.add(selectedIndex) 103 | } 104 | } else { 105 | if (selectedPositions.isNotEmpty()) { 106 | val temp = ArrayList(selectedPositions) 107 | selectedPositions.clear() 108 | temp.forEach { notifyItemChanged(it) } 109 | } 110 | selectedPositions.add(selectedIndex) 111 | } 112 | notifyItemChanged(selectedIndex) 113 | } 114 | 115 | companion object { 116 | private const val BACKGROUND_COLOR_NOT_SELECTED = "#FFFFFFFF" 117 | private const val BORDER_COLOR_NOT_SELECTED = "#61323232" 118 | } 119 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/lists/PdfViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.lists 2 | 3 | import android.graphics.Typeface 4 | import android.graphics.drawable.DrawableContainer.DrawableContainerState 5 | import android.graphics.drawable.GradientDrawable 6 | import android.util.TypedValue 7 | import android.view.View 8 | import android.widget.TextView 9 | import androidx.recyclerview.widget.RecyclerView 10 | import com.itextpdf.android.library.R 11 | import com.itextpdf.android.library.util.DisplayUtil 12 | import com.itextpdf.android.library.views.PdfThumbnailView 13 | 14 | 15 | /** 16 | * The view holder for an item in the pdf navigation view. 17 | * 18 | * @param view the view 19 | */ 20 | internal abstract class PdfViewHolder(view: View) : RecyclerView.ViewHolder(view) { 21 | 22 | protected val tvPageNumber: TextView = view.findViewById(R.id.tvPageNumber) 23 | protected val thumbnailView: PdfThumbnailView = view.findViewById(R.id.pageThumbnail) 24 | private val strokeWidth: Int = DisplayUtil.dpToPx(STROKE_WIDTH_IN_DP, itemView.context) 25 | 26 | abstract fun bind(item: PdfRecyclerItem) 27 | 28 | /** 29 | * Updates the text size of the view based on the selection state 30 | * 31 | * @param selected boolean flag whether the item is selected or not 32 | */ 33 | fun updateTextSize(selected: Boolean) { 34 | if (selected) { 35 | tvPageNumber.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f) 36 | tvPageNumber.setTypeface(null, Typeface.BOLD) 37 | } else { 38 | tvPageNumber.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f) 39 | tvPageNumber.setTypeface(null, Typeface.NORMAL) 40 | } 41 | } 42 | 43 | /** 44 | * Updates the border color of the item 45 | * 46 | * @param color the color int that should be used for the border 47 | */ 48 | fun updateBorderColor(color: Int) { 49 | val drawableContainerState = 50 | thumbnailView.background.constantState as DrawableContainerState? 51 | val children = drawableContainerState?.children 52 | if (!children.isNullOrEmpty()) { 53 | val selectedDrawable = children[0] as? GradientDrawable 54 | selectedDrawable?.setStroke(strokeWidth, color) 55 | } 56 | } 57 | 58 | companion object { 59 | private const val STROKE_WIDTH_IN_DP = 1f 60 | } 61 | } 62 | 63 | internal interface PdfRecyclerItem { 64 | val type: Int 65 | 66 | companion object { 67 | val TYPE_NAVIGATE = R.layout.recycler_item_navigation_pdf_page 68 | val TYPE_SPLIT = R.layout.recycler_item_split_pdf_page 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/lists/annotations/AnnotationsAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.lists.annotations 2 | 3 | import android.graphics.Color 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import androidx.core.graphics.drawable.DrawableCompat 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.itextpdf.android.library.R 9 | import com.itextpdf.android.library.extensions.isSameAs 10 | import com.itextpdf.android.library.lists.PdfRecyclerItem 11 | import com.itextpdf.android.library.lists.PdfRecyclerItem.Companion.TYPE_NAVIGATE 12 | import com.itextpdf.android.library.lists.PdfRecyclerItem.Companion.TYPE_SPLIT 13 | import com.itextpdf.android.library.lists.PdfViewHolder 14 | import com.itextpdf.android.library.lists.navigation.PdfNavigationViewHolder 15 | import com.itextpdf.android.library.lists.split.PdfSplitViewHolder 16 | import com.itextpdf.kernel.pdf.annot.PdfAnnotation 17 | 18 | /** 19 | * Adapter class for the annotations 20 | * 21 | * @property data a list of annotation items 22 | * 23 | * @param primaryColorString the primary color that is used for highlighting selected elements. optional 24 | * @param secondaryColorString the secondary color that is used for highlighting selected elements. optional 25 | */ 26 | internal class AnnotationsAdapter( 27 | private val data: List, 28 | primaryColorString: String?, 29 | secondaryColorString: String? 30 | ) : 31 | RecyclerView.Adapter() { 32 | 33 | var selectedPosition = 0 34 | 35 | private val primaryColor: Int? = if (primaryColorString != null) { 36 | Color.parseColor(primaryColorString) 37 | } else { 38 | null 39 | } 40 | private val secondaryColor: Int? = if (secondaryColorString != null) { 41 | Color.parseColor(secondaryColorString) 42 | } else { 43 | null 44 | } 45 | 46 | fun getPositionForAnnotation(annotation: PdfAnnotation): Int? { 47 | val index: Int = data.indexOfFirst { it.annotation.isSameAs(annotation) } 48 | 49 | return if (index == -1) { 50 | null 51 | } else { 52 | index 53 | } 54 | } 55 | 56 | fun getAnnotationForIndex(index: Int): PdfAnnotation? { 57 | return data.getOrNull(index)?.annotation 58 | } 59 | 60 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnnotationsViewHolder { 61 | val view = LayoutInflater.from(parent.context).inflate( 62 | R.layout.recycler_item_annotation, 63 | parent, 64 | false 65 | ) 66 | return AnnotationsViewHolder(view) 67 | } 68 | 69 | override fun getItemCount(): Int = data.size 70 | 71 | override fun onBindViewHolder(holder: AnnotationsViewHolder, position: Int) { 72 | val item = data[position] 73 | 74 | holder.bind(item) 75 | 76 | if (primaryColor != null && secondaryColor != null) { 77 | val background = holder.itemView.background 78 | DrawableCompat.setTint(background, secondaryColor) 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/lists/annotations/AnnotationsViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.lists.annotations 2 | 3 | import android.view.View 4 | import android.widget.ImageView 5 | import android.widget.TextView 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.itextpdf.android.library.R 8 | import com.itextpdf.android.library.lists.PdfRecyclerItem 9 | import com.itextpdf.android.library.lists.PdfViewHolder 10 | import com.itextpdf.kernel.pdf.annot.PdfAnnotation 11 | import com.shockwave.pdfium.PdfDocument 12 | import com.shockwave.pdfium.PdfiumCore 13 | 14 | 15 | /** 16 | * The view holder for an item in the annotations view. 17 | * 18 | * @param view the view 19 | */ 20 | internal class AnnotationsViewHolder(view: View): RecyclerView.ViewHolder(view) { 21 | 22 | private val tvTitle: TextView = view.findViewById(R.id.tvTitle) 23 | private val tvText: TextView = view.findViewById(R.id.tvText) 24 | private val ivMore: ImageView = view.findViewById(R.id.ivMore) 25 | 26 | fun bind(item: AnnotationRecyclerItem) { 27 | tvTitle.text = item.title 28 | tvText.text = item.text 29 | 30 | ivMore.setOnClickListener { 31 | item.action(it) 32 | } 33 | } 34 | } 35 | 36 | /** 37 | * The data class holding all the data required for an annotation 38 | * 39 | * @property action the action that should happen when the item is clicked 40 | */ 41 | internal data class AnnotationRecyclerItem( 42 | val annotation: PdfAnnotation, 43 | val title: String?, 44 | val text: String?, 45 | val action: (View) -> (Unit) 46 | ) -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/lists/highlighting/HighlightColorAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.lists.highlighting 2 | 3 | import android.graphics.Color 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.itextpdf.android.library.R 8 | 9 | /** 10 | * Adapter class for the highlight colors 11 | * 12 | * @property data a list of highlight color items 13 | */ 14 | internal class HighlightColorAdapter( 15 | private val data: List, 16 | primaryColorString: String? 17 | ) : 18 | RecyclerView.Adapter() { 19 | 20 | var selectedPosition = 0 21 | private set 22 | 23 | private val borderColorSelected: Int = if (primaryColorString != null) { 24 | Color.parseColor(primaryColorString) 25 | } else { 26 | Color.parseColor(BLACK_COLOR) 27 | } 28 | private val borderColorNotSelected: Int = Color.parseColor(TRANSPARENT_COLOR) 29 | 30 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HighlightColorViewHolder { 31 | val view = LayoutInflater.from(parent.context).inflate( 32 | R.layout.recycler_item_highlight_color, 33 | parent, 34 | false 35 | ) 36 | return HighlightColorViewHolder(view) 37 | } 38 | 39 | override fun getItemCount(): Int = data.size 40 | 41 | override fun onBindViewHolder(holder: HighlightColorViewHolder, position: Int) { 42 | val item = data[position] 43 | val selected = position == selectedPosition 44 | 45 | holder.bind(item) 46 | holder.itemView.isSelected = selected 47 | 48 | if (selected) { 49 | holder.updateBorderColor(borderColorSelected) 50 | } else { 51 | holder.updateBorderColor(borderColorNotSelected) 52 | } 53 | } 54 | 55 | fun updateSelectedItem(position: Int) { 56 | val oldSelection = selectedPosition 57 | selectedPosition = position 58 | notifyItemChanged(oldSelection) 59 | notifyItemChanged(selectedPosition) 60 | } 61 | 62 | companion object { 63 | private const val TRANSPARENT_COLOR = "#00000000" 64 | private const val BLACK_COLOR = "#000000" 65 | } 66 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/lists/highlighting/HighlightColorViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.lists.highlighting 2 | 3 | import android.graphics.Color 4 | import android.graphics.drawable.GradientDrawable 5 | import android.view.View 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.itextpdf.android.library.R 8 | import com.itextpdf.android.library.extensions.getHexString 9 | import com.itextpdf.android.library.util.DisplayUtil 10 | import com.itextpdf.kernel.colors.DeviceRgb 11 | 12 | 13 | /** 14 | * The view holder for an item in the highlighting view. 15 | * 16 | * @param view the view 17 | */ 18 | internal class HighlightColorViewHolder(view: View) : RecyclerView.ViewHolder(view) { 19 | 20 | private val circleView: View = view.findViewById(R.id.colorCircle) 21 | private val strokeWidth: Int = DisplayUtil.dpToPx(STROKE_WIDTH_IN_DP, itemView.context) 22 | 23 | fun bind(item: HighlightColorRecyclerItem) { 24 | val gradientDrawable = circleView.background as? GradientDrawable 25 | gradientDrawable?.setColor(Color.parseColor(item.color.getHexString())) 26 | 27 | itemView.setOnClickListener { 28 | item.action(item.color) 29 | } 30 | } 31 | 32 | /** 33 | * Updates the border color of the item 34 | * 35 | * @param color the color int that should be used for the border 36 | */ 37 | fun updateBorderColor(color: Int) { 38 | val selectedDrawable = circleView.background as? GradientDrawable 39 | selectedDrawable?.setStroke(strokeWidth, color) 40 | } 41 | 42 | companion object { 43 | private const val STROKE_WIDTH_IN_DP = 2f 44 | } 45 | } 46 | 47 | /** 48 | * The data class holding all the data required for a highlight color 49 | * 50 | * @property action the action that should happen when the item is clicked 51 | */ 52 | internal data class HighlightColorRecyclerItem( 53 | val color: DeviceRgb, 54 | val action: (DeviceRgb) -> (Unit) 55 | ) -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/lists/navigation/PdfNavigationViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.lists.navigation 2 | 3 | import android.view.View 4 | import com.itextpdf.android.library.lists.PdfRecyclerItem 5 | import com.itextpdf.android.library.lists.PdfViewHolder 6 | import com.shockwave.pdfium.PdfDocument 7 | import com.shockwave.pdfium.PdfiumCore 8 | 9 | 10 | /** 11 | * The view holder for an item in the pdf navigation view. 12 | * 13 | * @param view the view 14 | */ 15 | internal class PdfNavigationViewHolder(view: View) : PdfViewHolder(view) { 16 | 17 | override fun bind(item: PdfRecyclerItem) { 18 | if (item is PdfNavigationRecyclerItem) { 19 | val pageNumber = item.pageIndex + 1 20 | tvPageNumber.text = "$pageNumber" 21 | thumbnailView.setWithDocument(item.pdfiumCore, item.pdfDocument, item.pageIndex) 22 | 23 | itemView.setOnClickListener { 24 | item.action() 25 | } 26 | } 27 | } 28 | } 29 | 30 | /** 31 | * The data class holding all the data required for the pdf navigation 32 | * 33 | * @property pdfiumCore required for rendering the pdf page that needs to be displayed as a thumbnail 34 | * @property pdfDocument the pdfDocument that contains the page that should be rendered 35 | * @property pageIndex the index of the page within the pdfDocument 36 | * @property action the action that should happen when the item is clicked 37 | */ 38 | internal data class PdfNavigationRecyclerItem( 39 | val pdfiumCore: PdfiumCore, 40 | val pdfDocument: PdfDocument, 41 | val pageIndex: Int, 42 | val action: () -> Unit 43 | ) : PdfRecyclerItem { 44 | override val type: Int 45 | get() = PdfRecyclerItem.TYPE_NAVIGATE 46 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/lists/split/PdfSplitViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.lists.split 2 | 3 | import android.graphics.Bitmap 4 | import android.view.View 5 | import com.itextpdf.android.library.lists.PdfRecyclerItem 6 | import com.itextpdf.android.library.lists.PdfViewHolder 7 | 8 | 9 | /** 10 | * The view holder for an item in the pdf split view. 11 | * 12 | * @param view the view 13 | */ 14 | internal class PdfSplitViewHolder(view: View) : PdfViewHolder(view) { 15 | 16 | override fun bind(item: PdfRecyclerItem) { 17 | if (item is PdfSplitRecyclerItem) { 18 | val pageNumber = item.pageIndex + 1 19 | tvPageNumber.text = "$pageNumber" 20 | thumbnailView.pdfImageView.setImageBitmap(item.bitmap) 21 | 22 | itemView.setOnClickListener { 23 | item.action() 24 | } 25 | } 26 | } 27 | } 28 | 29 | /** 30 | * The data class holding all the data required for the pdf splitting 31 | * 32 | * @property pageIndex the index of the page within the pdfDocument 33 | * @property action the action that should happen when the item is clicked 34 | */ 35 | internal data class PdfSplitRecyclerItem( 36 | var bitmap: Bitmap, 37 | val pageIndex: Int, 38 | val action: () -> Unit 39 | ) : PdfRecyclerItem { 40 | override val type: Int 41 | get() = PdfRecyclerItem.TYPE_SPLIT 42 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/paging/Page.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.paging 2 | 3 | internal class Page( 4 | val content: List, 5 | val size: Int, 6 | val number: Int, 7 | val totalPages: Int 8 | ) -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/paging/PaginationScrollListener.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.paging 2 | 3 | import androidx.recyclerview.widget.LinearLayoutManager 4 | import androidx.recyclerview.widget.RecyclerView 5 | 6 | internal abstract class PaginationScrollListener( 7 | private val layoutManager: LinearLayoutManager, 8 | /** 9 | * If there are only x more items before the end of the recycler, load more. 10 | */ 11 | private val loadMoreOffset: Int 12 | ) : 13 | RecyclerView.OnScrollListener() { 14 | 15 | override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { 16 | super.onScrolled(recyclerView, dx, dy) 17 | 18 | val visibleItemCount = layoutManager.childCount 19 | val totalItemCount = layoutManager.itemCount 20 | val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() 21 | 22 | if (!isLoading && !isLastPage) { 23 | val shouldLoadMoreCount = visibleItemCount + firstVisibleItemPosition + loadMoreOffset 24 | if (shouldLoadMoreCount >= totalItemCount && firstVisibleItemPosition >= 0) { 25 | loadMoreItems() 26 | } 27 | } 28 | } 29 | 30 | protected abstract fun loadMoreItems() 31 | abstract val isLastPage: Boolean 32 | abstract val isLoading: Boolean 33 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/util/DisplayUtil.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.util 2 | 3 | import android.content.Context 4 | import android.util.TypedValue 5 | import kotlin.math.roundToInt 6 | 7 | 8 | internal object DisplayUtil { 9 | /** 10 | * Converts dp to pixels. 11 | * 12 | * @param dp the dp value that should be converted to pixels 13 | * @param context the context that is used to get the displayMetrics 14 | * @return the passed dp value converted to pixels 15 | */ 16 | fun dpToPx(dp: Float, context: Context): Int { 17 | return TypedValue.applyDimension( 18 | TypedValue.COMPLEX_UNIT_DIP, 19 | dp, 20 | context.resources.displayMetrics 21 | ).roundToInt() 22 | } 23 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/util/FileUtil.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.util 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import java.io.File 6 | 7 | internal interface FileUtil { 8 | 9 | fun createTempCopy(context: Context, originalFile: File): File 10 | fun createTempCopyIfNotExists(context: Context, originalFileUri: Uri): File 11 | 12 | fun overrideFile(fileToSave: File, destinationUri: Uri): File 13 | fun loadFileFromAssets(context: Context, fileName: String): File 14 | 15 | companion object { 16 | private lateinit var instance: FileUtil 17 | 18 | fun getInstance(): FileUtil { 19 | if (!this::instance.isInitialized) { 20 | instance = FileUtilImpl() 21 | } 22 | return instance 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/util/FileUtilImpl.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.util 2 | 3 | import android.content.Context 4 | import android.content.res.AssetManager 5 | import android.net.Uri 6 | import android.util.Log 7 | import com.itextpdf.android.library.extensions.getFileName 8 | import java.io.* 9 | import java.nio.file.Files 10 | 11 | internal class FileUtilImpl : FileUtil { 12 | 13 | /** 14 | * Loads a file with the given fileName from the assets folder to a location the app can access and 15 | * returns the absolute path to that file if the operation was successful or throws an IOException 16 | * if something went wrong. 17 | * 18 | * @param fileName the name of the file that should be loaded from the assets folder 19 | * @return the file 20 | * @throws IOException 21 | */ 22 | @Throws(IOException::class) 23 | override fun loadFileFromAssets(context: Context, fileName: String): File { 24 | // create file object to read and write on in the cache directory of the app 25 | val file = File(context.cacheDir, fileName) 26 | if (!file.exists()) { 27 | val assetManager: AssetManager = context.assets 28 | // copy pdf file from assets to location of the previously created file 29 | copyAsset(assetManager, fileName, file.absolutePath) 30 | } 31 | return file 32 | } 33 | 34 | /** 35 | * Utility function to copy a file from the assets folder to a provided path 36 | * 37 | * @param assetManager the assetManager that is required to open files from the assets 38 | * @param fileName the name of the file that should be copied from assets to the given path 39 | * @param toPath the path where the file from the assets should be copied to 40 | * @return true if the operation was successful, false if not 41 | */ 42 | private fun copyAsset( 43 | assetManager: AssetManager, 44 | fileName: String, 45 | toPath: String? 46 | ): Boolean { 47 | var inputStream: InputStream? 48 | var outputStream: OutputStream? 49 | return try { 50 | inputStream = assetManager.open(fileName) 51 | File(toPath).createNewFile() 52 | outputStream = FileOutputStream(toPath) 53 | copyFile(inputStream, outputStream) 54 | inputStream.close() 55 | inputStream = null 56 | outputStream.flush() 57 | outputStream.close() 58 | outputStream = null 59 | true 60 | } catch (e: Exception) { 61 | Log.e(LOG_TAG, null, e) 62 | false 63 | } 64 | } 65 | 66 | /** 67 | * Copies a file from input to output stream 68 | * 69 | * @param in the input stream 70 | * @param out the output stream 71 | * @throws IOException 72 | */ 73 | @Throws(IOException::class) 74 | private fun copyFile(`in`: InputStream, out: OutputStream) { 75 | val buffer = ByteArray(1024) 76 | var read: Int 77 | while (`in`.read(buffer).also { read = it } != -1) { 78 | out.write(buffer, 0, read) 79 | } 80 | } 81 | 82 | override fun overrideFile(fileToSave: File, destinationUri: Uri): File { 83 | val existingFile = File(destinationUri.path) 84 | FileOutputStream(existingFile, false).use { overWrite -> 85 | overWrite.write(fileToSave.readBytes()) 86 | overWrite.flush() 87 | } 88 | return existingFile 89 | } 90 | 91 | override fun createTempCopy(context: Context, originalFile: File): File { 92 | val storageFolderPath = 93 | (context.externalCacheDir ?: context.cacheDir).absolutePath 94 | return File("$storageFolderPath/temp_${originalFile.name}") 95 | } 96 | 97 | override fun createTempCopyIfNotExists(context: Context, originalFileUri: Uri): File { 98 | 99 | val originalFileName = context.getFileName(originalFileUri) 100 | 101 | val storageFolderPath = (context.externalCacheDir ?: context.cacheDir).absolutePath 102 | val file = File("$storageFolderPath/temp_${originalFileName}") 103 | 104 | if (!file.exists()) { 105 | context.contentResolver.openInputStream(originalFileUri)!!.use { inputStream -> 106 | Files.copy(inputStream, file.toPath()) 107 | } 108 | } 109 | 110 | return file 111 | } 112 | 113 | companion object { 114 | private const val LOG_TAG = "FileUtilImpl" 115 | } 116 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/util/ImageUtil.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.util 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.Color 6 | import androidx.annotation.ColorInt 7 | import androidx.annotation.DrawableRes 8 | import androidx.appcompat.content.res.AppCompatResources 9 | import androidx.core.graphics.drawable.DrawableCompat 10 | import androidx.core.graphics.drawable.toBitmap 11 | import java.io.ByteArrayOutputStream 12 | 13 | internal object ImageUtil { 14 | 15 | fun getResourceAsBitmap(context: Context, @DrawableRes resId: Int, imageSize: Int, @ColorInt tintColor: Int): Bitmap? { 16 | val d = AppCompatResources.getDrawable( 17 | context, 18 | resId 19 | )?.constantState?.newDrawable()?.mutate() 20 | if (d != null) { 21 | val wrappedDrawable = DrawableCompat.wrap(d) 22 | DrawableCompat.setTint(wrappedDrawable, tintColor) 23 | 24 | return d.toBitmap(imageSize, imageSize, Bitmap.Config.ARGB_8888) 25 | } 26 | return null 27 | } 28 | 29 | fun getResourceAsByteArray(context: Context, @DrawableRes resId: Int, imageSize: Int, @ColorInt tintColor: Int): ByteArray? { 30 | val bitmap = getResourceAsBitmap(context, resId, imageSize, tintColor) 31 | return if (bitmap != null) { 32 | val stream = ByteArrayOutputStream() 33 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) 34 | stream.toByteArray() 35 | } else { 36 | null 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/util/PdfManipulator.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.util 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import androidx.annotation.ColorInt 6 | import com.itextpdf.kernel.colors.Color 7 | import com.itextpdf.kernel.geom.Rectangle 8 | import com.itextpdf.kernel.pdf.PdfDocument 9 | import com.itextpdf.kernel.pdf.annot.PdfAnnotation 10 | import com.itextpdf.kernel.utils.PageRange 11 | import java.io.File 12 | 13 | /** 14 | * A manipulator provides several functions for manipulating PDF files, such as adding text-annotations and splitting pdf-documents. 15 | */ 16 | interface PdfManipulator { 17 | 18 | /** 19 | * The working-copy of the currently edited PDF document. 20 | */ 21 | val workingCopy: File 22 | 23 | /** 24 | * Splits the pdf file at the given uri and creates a new document with the selected page indices and another one for the unselected indices. 25 | * If selected page indices are empty or contains all the pages, there will only be one document with all pages. 26 | * 27 | * @param fileName the name of the file that will be split. Only relevant for naming the new split documents. 28 | * @param selectedPageIndices the list of selected page indices that will be used to create a document with selected and another document 29 | * with not selected pages. 30 | * @param storageFolderPath the path where the newly created pdf files will be stored 31 | * @return the list of uris of the newly created split documents 32 | */ 33 | fun splitPdfWithSelection(fileName: String, selectedPageIndices: List, storageFolderPath: String): List 34 | 35 | /** 36 | * Creates the name for the new document created during the split based on initial name and partNumber 37 | * 38 | * @param initialFileName the name of the original document 39 | * @param partNumber the part number of the document for which a name should be created. 1 is the first document, 2 the second, ... 40 | * @param selectedPagesNumbers the list of selected page numbers 41 | * @param unselectedPageNumbers the list of unselected page numbers 42 | * @return the name for the split document 43 | */ 44 | fun getSplitDocumentName(initialFileName: String, partNumber: Int, selectedPagesNumbers: List, unselectedPageNumbers: List): String 45 | 46 | /** 47 | * Returns the page ranges for selected and unselected pages that can be used for splitting 48 | * 49 | * @param selectedPagesNumbers a list of page numbers (NOT indices) that were selected 50 | * @param unselectedPageNumbers a list of page numbers (NOT indices) that were not selected 51 | * @param numberOfPages the number of pages this pdf document has 52 | * @return the list of page ranges (can be empty if selected pages and unselected pages were empty or the numbers were higher than the numberOfPages) 53 | */ 54 | fun getPageRanges(selectedPagesNumbers: List, unselectedPageNumbers: List, numberOfPages: Int): List 55 | 56 | /** 57 | * Adds a text annotation to the PDF. 58 | * 59 | * @param title The title of the annotation. 60 | * @param text The text of the annotation. 61 | * @param pageIndex The zero-based page-index, specifying on which PDF page the annotation shall be added. 62 | * @param x The x-coordinates of the annotation. 63 | * @param y The y-coordinate of the annotation. 64 | * @param bubbleSize The size of the highlight-bubble. 65 | * @param bubbleColor The color of the highlight-bubble. 66 | */ 67 | fun addTextAnnotationToPdf(title: String?, text: String, pageIndex: Int, x: Float, y: Float, bubbleSize: Float, @ColorInt bubbleColor: Int): File 68 | 69 | /** 70 | * Ads a markup annotation with [rect] and [color] on the given [pageIndex]. 71 | */ 72 | fun addMarkupAnnotationToPdf(pageIndex: Int, rect: Rectangle, color: Color): File 73 | 74 | /** 75 | * Remove the given [annotationToRemove] from the PDF file. 76 | * 77 | * @param annotationToRemove The annotation to be removed. 78 | * @return The updated PDF file 79 | */ 80 | fun removeAnnotationFromPdf(annotationToRemove: PdfAnnotation): File 81 | 82 | /** 83 | * Updates [title] and [text] of the specified [annotation]. 84 | * 85 | * @param annotation The annotation to be edited. 86 | * @param title The title to be set for the annotation. 87 | * @param text The text to be set for the annotation. 88 | * @return The updated PDF file. 89 | */ 90 | fun editAnnotationFromPdf(annotation: PdfAnnotation, title: String?, text: String): File 91 | 92 | /** 93 | * Returns the [PdfDocument] in reading-mode. 94 | */ 95 | fun getPdfDocumentInReadingMode(): PdfDocument 96 | 97 | /** 98 | * Returns the [PdfDocument] in stamping-mode. 99 | */ 100 | fun getPdfDocumentInStampingMode(destFile: File): PdfDocument 101 | 102 | /** 103 | * Factory for creating [PdfManipulator] instances. 104 | */ 105 | companion object Factory { 106 | 107 | /** 108 | * Returns a new instance of [PdfManipulator] for the given pdf-file located at [pdfUri]. 109 | * 110 | * @param context The context to be used for accessing resources etc. 111 | * @param pdfUri The uri pointing at the location where the PDF file is located. 112 | */ 113 | @JvmStatic 114 | fun create(context: Context, pdfUri: Uri): PdfManipulator { 115 | return PdfManipulatorImpl(context, pdfUri) 116 | } 117 | } 118 | 119 | 120 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/util/PointPositionMappingInfo.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.util 2 | 3 | import android.graphics.PointF 4 | import android.view.MotionEvent 5 | import com.github.barteksc.pdfviewer.PDFView 6 | import com.itextpdf.android.library.extensions.convertMotionEventPointToPdfPagePoint 7 | import com.itextpdf.android.library.extensions.getPageIndexForClickPosition 8 | 9 | /** 10 | * Class that contains mapping-information regarding the initiating [motionEvent] (in device-coordinates) and the corresponding [pdfCoordinates] and [pdfPageIndex]. 11 | * 12 | */ 13 | internal data class PointPositionMappingInfo( 14 | 15 | /** 16 | * The pdf-coordinates of the related [motionEvent] 17 | */ 18 | val pdfCoordinates: PointF, 19 | 20 | /** 21 | * The corresponding (zero-based) pdf-page index where the [motionEvent] occurred. 22 | */ 23 | val pdfPageIndex: Int, 24 | 25 | /** 26 | * The initiating motion-event. 27 | */ 28 | val motionEvent: MotionEvent 29 | ) { 30 | 31 | companion object Factory { 32 | fun createOrNull(event: MotionEvent, pdfView: PDFView): PointPositionMappingInfo? { 33 | 34 | val pdfCoordinates: PointF = pdfView.convertMotionEventPointToPdfPagePoint(event) ?: return null 35 | val pdfPageIndex: Int = pdfView.getPageIndexForClickPosition(event) ?: return null 36 | 37 | return PointPositionMappingInfo(pdfCoordinates = pdfCoordinates, motionEvent = event, pdfPageIndex = pdfPageIndex) 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/util/RectanglePositionMappingInfo.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.util 2 | 3 | import com.github.barteksc.pdfviewer.PDFView 4 | import com.itextpdf.android.library.extensions.convertScreenRectToPdfPageRect 5 | import com.itextpdf.android.library.extensions.getPageIndexAtScreenPoint 6 | import com.itextpdf.kernel.geom.Rectangle 7 | 8 | /** 9 | * Class that contains mapping-information regarding the initiating [screenRectangle] (in device-coordinates) and the corresponding [pdfRectangle] and [pdfPageIndex]. 10 | * 11 | */ 12 | internal data class RectanglePositionMappingInfo( 13 | 14 | /** 15 | * The rectangle in the pdf coordinate system related to the [screenRectangle] 16 | */ 17 | val pdfRectangle: Rectangle, 18 | 19 | /** 20 | * The corresponding (zero-based) pdf-page index where the [screenRectangle] occurred. 21 | */ 22 | val pdfPageIndex: Int, 23 | 24 | /** 25 | * The initiating rectangle in screen coordinate system. 26 | */ 27 | val screenRectangle: Rectangle 28 | ) { 29 | 30 | companion object Factory { 31 | fun createOrNull(screenRectangle: Rectangle, pdfView: PDFView): RectanglePositionMappingInfo? { 32 | 33 | // calculate the page index based on the top left corner of the rect 34 | val pdfPageIndex: Int = pdfView.getPageIndexAtScreenPoint(screenRectangle.x, screenRectangle.y) ?: return null 35 | val pdfRect = pdfView.convertScreenRectToPdfPageRect(screenRectangle) ?: return null 36 | 37 | return RectanglePositionMappingInfo(pdfRectangle = pdfRect, pdfPageIndex = pdfPageIndex, screenRectangle = screenRectangle) 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /library/src/main/java/com/itextpdf/android/library/views/PdfThumbnailView.kt: -------------------------------------------------------------------------------- 1 | package com.itextpdf.android.library.views 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.net.Uri 6 | import android.os.ParcelFileDescriptor 7 | import android.util.AttributeSet 8 | import android.util.Log 9 | import android.view.LayoutInflater 10 | import android.widget.FrameLayout 11 | import android.widget.ImageView 12 | import com.itextpdf.android.library.R 13 | import com.shockwave.pdfium.PdfDocument 14 | import com.shockwave.pdfium.PdfiumCore 15 | import java.io.File 16 | import java.lang.Integer.min 17 | 18 | 19 | /** 20 | * View that easily allows to display a thumbnail for a pdf file by setting it as file or uri. 21 | * 22 | * @constructor Constructor for creating a new [PdfThumbnailView] instance. 23 | * 24 | * @param context The context of the view. 25 | * @param attrs The attributes of the view. 26 | */ 27 | class PdfThumbnailView constructor(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) { 28 | 29 | /** 30 | * The image view that is used tho display the thumbnail of the pdf page. 31 | */ 32 | val pdfImageView: ImageView 33 | 34 | init { 35 | val inflater = LayoutInflater.from(context) 36 | inflater.inflate(R.layout.view_pdf_thumbnail, this, true) 37 | 38 | // val a = context.obtainStyledAttributes( 39 | // attrs, 40 | // R.styleable.TextInputView, 0, 0 41 | // ) 42 | 43 | pdfImageView = findViewById(R.id.imageViewPdf) 44 | 45 | // a.recycle() // recycle for re-use (required) 46 | } 47 | 48 | /** 49 | * Sets a pdf file as the source of this thumbnail view that is rendered and displayed. By setting 50 | * a pageIndex, it is possible to define which page of the pdf file should be used for the thumbnail. 51 | * After rendering the created PdfDocument is closed again. 52 | * 53 | * @param file the pdf file 54 | * @param pageIndex the index of the page that should be used as thumbnail. default: 0 55 | */ 56 | fun set(file: File, pageIndex: Int = 0) { 57 | 58 | post { 59 | val fileDescriptor: ParcelFileDescriptor = 60 | ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) 61 | setImageViewWithFileDescriptor(fileDescriptor, pageIndex) 62 | } 63 | } 64 | 65 | /** 66 | * Sets an uri of a pdf file as the source of this thumbnail view that is rendered and displayed. By setting 67 | * a pageIndex, it is possible to define which page of the pdf file should be used for the thumbnail. 68 | * After rendering the created pdfDocument is closed again. 69 | * 70 | * @param uri the uri of the pdf file 71 | * @param pageIndex the index of the page that should be used as thumbnail. default: 0 72 | */ 73 | fun set(uri: Uri, pageIndex: Int = 0) { 74 | post { 75 | val fileDescriptor: ParcelFileDescriptor? = 76 | context.contentResolver.openFileDescriptor(uri, "r") 77 | fileDescriptor?.let { 78 | setImageViewWithFileDescriptor(fileDescriptor, pageIndex) 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * Uses the pdfiumCore and a pdfDocument to render the page with the given index to display it 85 | * in this thumbnail view. By setting a pageIndex, it is possible to define which page of the pdf file 86 | * should be used for the thumbnail. 87 | * After rendering the pdfDocument is not closed. This makes it possible to render multiple pages 88 | * of the same pdfDocument more efficiently. 89 | * Make sure to manually close the document after rendering to avoid memory issues. 90 | * 91 | * @param pdfiumCore the pdfiumCore object used for the rendering 92 | * @param pdfDocument the pdfDocument from which a specific page should be rendered 93 | * @param pageIndex the index of the page that should be used as a thumbnail. 94 | */ 95 | fun setWithDocument(pdfiumCore: PdfiumCore, pdfDocument: PdfDocument, pageIndex: Int) { 96 | post { 97 | setImageViewWithPdfDocument(pdfiumCore, pdfDocument, pageIndex) 98 | } 99 | } 100 | 101 | /** 102 | * Creates a new instance of the pdfiumCore and uses the fileDescriptor to create a new pdfDocument. 103 | * Gets the desired page from the pdfDocument and renders it with the help of the pdfiumCore. 104 | * The resulting bitmap is loaded into the pdfImageView. 105 | * After rendering the created pdfDocument is closed again. 106 | * 107 | * @param fileDescriptor the fileDescriptor needed to create the pdfDocument 108 | * @param pageIndex the index of the page that should be used as a thumbnail. 109 | */ 110 | private fun setImageViewWithFileDescriptor( 111 | fileDescriptor: ParcelFileDescriptor, 112 | pageIndex: Int 113 | ) { 114 | val pdfiumCore = PdfiumCore(context) 115 | try { 116 | val pdfDocument = pdfiumCore.newDocument(fileDescriptor) 117 | setImageViewWithPdfDocument(pdfiumCore, pdfDocument, pageIndex) 118 | pdfiumCore.closeDocument(pdfDocument) 119 | } catch (exception: Exception) { 120 | Log.e(LOG_TAG, null, exception) 121 | } 122 | } 123 | 124 | /** 125 | * Gets the desired page from the pdfDocument and renders it with the help of the pdfiumCore. 126 | * The resulting bitmap is loaded into the pdfImageView. 127 | * After rendering the pdfDocument is not closed. This makes it possible to render multiple pages 128 | * of the same pdfDocument more efficiently. 129 | * Make sure to manually close the document after rendering to avoid memory issues. 130 | * 131 | * @param pdfiumCore the pdfiumCore object used for the rendering 132 | * @param pdfDocument the pdfDocument from which a specific page should be rendered 133 | * @param pageIndex the index of the page that should be used as a thumbnail. 134 | */ 135 | private fun setImageViewWithPdfDocument( 136 | pdfiumCore: PdfiumCore, 137 | pdfDocument: PdfDocument, 138 | pageIndex: Int 139 | ) { 140 | pdfiumCore.openPage(pdfDocument, pageIndex) 141 | 142 | val width = min(width, pdfiumCore.getPageWidthPoint(pdfDocument, pageIndex)) 143 | val height = min(height, pdfiumCore.getPageHeightPoint(pdfDocument, pageIndex)) 144 | 145 | val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565) 146 | pdfiumCore.renderPageBitmap(pdfDocument, bitmap, pageIndex, 0, 0, width, height, true) 147 | 148 | pdfImageView.setImageBitmap(bitmap) 149 | } 150 | 151 | internal companion object { 152 | private const val LOG_TAG = "PdfThumbnailView" 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /library/src/main/res/drawable-xhdpi/background_rounded_light_orange.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/drawable-xhdpi/background_slightly_rounded_light_orange.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | -------------------------------------------------------------------------------- /library/src/main/res/drawable-xhdpi/border_background_grey.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 12 | 17 | -------------------------------------------------------------------------------- /library/src/main/res/drawable-xhdpi/border_background_orange.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 12 | 17 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/circle_shape_with_border.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_annotation.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_arrow_left.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_arrow_right.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_check.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_edit.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_help_outline.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_highlight.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_more_horizontal.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_navigate.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_send.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_speech_bubble.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_speech_bubble_old.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_split.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/navigation_page_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/navigation_page_border_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/scroll_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /library/src/main/res/layout/activity_pdf.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 22 | 23 | -------------------------------------------------------------------------------- /library/src/main/res/layout/activity_split_pdf.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 21 | 22 | -------------------------------------------------------------------------------- /library/src/main/res/layout/bottom_sheet_annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 20 | 21 | 31 | 32 | 40 | 41 | 50 | 51 | 52 | 60 | 61 | 70 | 71 | 75 | 76 | 77 | 86 | 87 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /library/src/main/res/layout/bottom_sheet_highlight.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 20 | 21 | 31 | 32 | -------------------------------------------------------------------------------- /library/src/main/res/layout/bottom_sheet_navigate.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 20 | 21 | 30 | 31 | -------------------------------------------------------------------------------- /library/src/main/res/layout/custom_scroll_handle.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | 25 | 26 | 34 | 35 | 36 | 37 | 38 | 48 | 49 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /library/src/main/res/layout/fragment_pdf.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 20 | 21 | 22 | 28 | 29 | 35 | 36 | 42 | 43 | 46 | 47 | 50 | 51 | 54 | 55 | 64 | 65 | 72 | 73 | 87 | 88 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /library/src/main/res/layout/fragment_split_document.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 20 | 21 | 22 | 30 | 31 | 37 | 38 | 50 | 51 | -------------------------------------------------------------------------------- /library/src/main/res/layout/recycler_item_annotation.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 26 | 27 | 35 | 36 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /library/src/main/res/layout/recycler_item_highlight_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | -------------------------------------------------------------------------------- /library/src/main/res/layout/recycler_item_navigation_pdf_page.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | 25 | 26 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /library/src/main/res/layout/recycler_item_split_pdf_page.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 19 | 20 | 26 | 27 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /library/src/main/res/layout/view_pdf_thumbnail.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | -------------------------------------------------------------------------------- /library/src/main/res/menu/menu_confirm.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/menu/menu_pdf_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 16 | 17 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /library/src/main/res/menu/menu_split_document.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/menu/popup_menu_annotation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | -------------------------------------------------------------------------------- /library/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /library/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FEF8F0 4 | #FFEFD8 5 | #FFB145 6 | #F79918 7 | #ECA144 8 | #FF000000 9 | #FFFFFFFF 10 | #EAEAEA 11 | #B1B1B1 12 | #61323232 13 | #3C3C3C 14 | -------------------------------------------------------------------------------- /library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Navigate PDF 4 | Split document 5 | Successfully split document 6 | There was an error splitting the document. 7 | Help 8 | Help 9 | To split a document, select the pages you would like to extract from the PDF file. The selected pages will be highlighted.\n\nWhen you are done selecting, press the split button at the bottom right to initiate the splitting process.\n\nAfter the process is complete, the split view will be closed and the result is two PDF files. One with all the selected pages and another one with the unselected pages. The original PDF file remains unchanged. 10 | Annotations 11 | Save 12 | Add a note 13 | This file doesn\'t have any annotations. 14 | You can add one by long-pressing the PDF document at the location where you want to add the annotation. 15 | Edit 16 | Delete 17 | Confirm 18 | Highlight 19 | Close 20 | 21 | Unsaved changes 22 | Are you done editing and want to keep your changes? 23 | Yes, accept changes 24 | Discard changes 25 | Keep editing 26 | Error while adding text annotation. 27 | Error while adding markup annotation. 28 | Error while editing annotation. 29 | Error while removing annotation. 30 | 31 | -------------------------------------------------------------------------------- /library/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | -------------------------------------------------------------------------------- /library/src/main/res/xml/provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /library/src/test/assets/sample_1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/library/src/test/assets/sample_1.pdf -------------------------------------------------------------------------------- /library/src/test/assets/sample_2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/library/src/test/assets/sample_2.pdf -------------------------------------------------------------------------------- /library/src/test/assets/sample_3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/library/src/test/assets/sample_3.pdf -------------------------------------------------------------------------------- /library/src/test/assets/sample_4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itext/itext-android-ui/c3070dab579f55833bb210c65b88245b2a93576a/library/src/test/assets/sample_4.pdf -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "itext7-android-ui" 2 | include ':app' 3 | include ':library' 4 | 5 | --------------------------------------------------------------------------------