├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | xmlns:android
18 |
19 | ^$
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | xmlns:.*
29 |
30 | ^$
31 |
32 |
33 | BY_NAME
34 |
35 |
36 |
37 |
38 |
39 |
40 | .*:id
41 |
42 | http://schemas.android.com/apk/res/android
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | .*:name
52 |
53 | http://schemas.android.com/apk/res/android
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | name
63 |
64 | ^$
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | style
74 |
75 | ^$
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | .*
85 |
86 | ^$
87 |
88 |
89 | BY_NAME
90 |
91 |
92 |
93 |
94 |
95 |
96 | .*
97 |
98 | http://schemas.android.com/apk/res/android
99 |
100 |
101 | ANDROID_ATTRIBUTE_ORDER
102 |
103 |
104 |
105 |
106 |
107 |
108 | .*
109 |
110 | .*
111 |
112 |
113 | BY_NAME
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/library/src/main/res/menu/menu_pdf_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/library/src/main/res/menu/menu_split_document.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/library/src/main/res/menu/popup_menu_annotation.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------