├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── JetPDFVue ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── pratikk │ └── jetpdfvue │ ├── BlankPage.kt │ ├── CustomSlider.kt │ ├── HorizontalVueReader.kt │ ├── VerticalVueReader.kt │ ├── VueRenderer.kt │ ├── VueSlider.kt │ ├── network │ └── VueDownloader.kt │ ├── state │ ├── HorizontalVueReaderState.kt │ ├── PagerState.kt │ ├── VerticalVueReaderState.kt │ ├── VueFilePicker.kt │ ├── VueImportState.kt │ ├── VueLoadState.kt │ ├── VuePageState.kt │ ├── VueReaderState.kt │ └── VueResourceType.kt │ └── util │ ├── PDFUtil.kt │ └── VueFileExtensions.kt ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── pratikk │ │ └── jetpackpdf │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── demo.jpg │ │ ├── lorem_ipsum.pdf │ │ └── lorem_ipsum_base64.txt │ ├── java │ │ └── com │ │ │ └── pratikk │ │ │ └── jetpackpdf │ │ │ ├── MainActivity.kt │ │ │ ├── horizontalSamples │ │ │ ├── HorizontalPdfViewer.kt │ │ │ ├── HorizontalSampleA.kt │ │ │ └── HorizontalSampleB.kt │ │ │ ├── ui │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── verticalSamples │ │ │ ├── VerticalPdfViewer.kt │ │ │ └── VerticalSampleA.kt │ └── res │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── ic_launcher_foreground.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 │ │ ├── raw │ │ ├── demo.jpg │ │ ├── lorem_ipsum.pdf │ │ └── lorem_ipsum_base64.txt │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ └── provider_paths.xml │ └── test │ └── java │ └── com │ └── pratikk │ └── jetpackpdf │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | /.idea/inspectionProfiles/Project_Default.xml 17 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 32 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /JetPDFVue/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /JetPDFVue/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kotlin-android") 3 | id("maven-publish") 4 | id("com.android.library") 5 | id("org.jetbrains.kotlin.android") 6 | id("kotlin-parcelize") 7 | } 8 | 9 | android { 10 | namespace = "com.pratikk.jetpdfvue" 11 | compileSdk = 35 12 | 13 | defaultConfig { 14 | minSdk = 21 15 | 16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 17 | consumerProguardFiles("consumer-rules.pro") 18 | } 19 | 20 | buildTypes { 21 | release { 22 | isMinifyEnabled = false 23 | proguardFiles( 24 | getDefaultProguardFile("proguard-android-optimize.txt"), 25 | "proguard-rules.pro" 26 | ) 27 | } 28 | } 29 | composeOptions { 30 | kotlinCompilerExtensionVersion = "1.5.1" 31 | } 32 | compileOptions { 33 | sourceCompatibility = JavaVersion.VERSION_17 34 | targetCompatibility = JavaVersion.VERSION_17 35 | } 36 | kotlinOptions { 37 | jvmTarget = "17" 38 | } 39 | buildFeatures { 40 | compose = true 41 | } 42 | configure { 43 | publications.create("JetPDFVue") { 44 | groupId = "com.github.pratikksahu" 45 | artifactId = "JetPDFVue" 46 | version = "1.0.0" 47 | afterEvaluate { 48 | from(components["release"]) 49 | } 50 | } 51 | } 52 | } 53 | dependencies { 54 | implementation("androidx.activity:activity-compose:1.9.3") 55 | implementation(platform("androidx.compose:compose-bom:2024.10.01")) 56 | implementation("androidx.compose.ui:ui") 57 | implementation("androidx.compose.ui:ui-graphics") 58 | implementation("androidx.compose.ui:ui-tooling-preview") 59 | implementation("androidx.compose.material3:material3") 60 | implementation("androidx.compose.foundation:foundation-android:1.7.5") 61 | 62 | implementation("androidx.core:core-ktx:1.15.0") 63 | 64 | implementation("androidx.compose.material:material-icons-extended") 65 | implementation("androidx.compose.material3:material3-window-size-class") 66 | } -------------------------------------------------------------------------------- /JetPDFVue/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratikksahu/JetPDFVue/c763f7b84fc44ac336b4af697911409a75aea009/JetPDFVue/consumer-rules.pro -------------------------------------------------------------------------------- /JetPDFVue/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /JetPDFVue/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /JetPDFVue/src/main/java/com/pratikk/jetpdfvue/BlankPage.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpdfvue 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.unit.dp 10 | 11 | @Composable 12 | fun BlankPage( 13 | modifier: Modifier = Modifier, 14 | width: Int, 15 | height: Int 16 | ) { 17 | Box( 18 | modifier = modifier 19 | .size( 20 | width = width.dp, 21 | height = height.dp 22 | ) 23 | .background(color = Color.White) 24 | ) 25 | } -------------------------------------------------------------------------------- /JetPDFVue/src/main/java/com/pratikk/jetpdfvue/CustomSlider.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpdfvue 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.defaultMinSize 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.heightIn 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.shape.CircleShape 10 | import androidx.compose.material3.ExperimentalMaterial3Api 11 | import androidx.compose.material3.Slider 12 | import androidx.compose.material3.SliderState 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.clip 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.graphics.Shape 20 | import androidx.compose.ui.graphics.TransformOrigin 21 | import androidx.compose.ui.graphics.graphicsLayer 22 | import androidx.compose.ui.layout.Layout 23 | import androidx.compose.ui.layout.MeasurePolicy 24 | import androidx.compose.ui.layout.layout 25 | import androidx.compose.ui.layout.layoutId 26 | import androidx.compose.ui.text.TextStyle 27 | import androidx.compose.ui.text.font.FontWeight 28 | import androidx.compose.ui.text.style.TextAlign 29 | import androidx.compose.ui.unit.Constraints 30 | import androidx.compose.ui.unit.Dp 31 | import androidx.compose.ui.unit.dp 32 | import androidx.compose.ui.unit.sp 33 | import kotlin.math.roundToInt 34 | 35 | /** 36 | * A custom slider composable that allows selecting a value within a given range. 37 | * 38 | * @param value The current value of the slider. 39 | * @param onValueChange The callback invoked when the value of the slider changes. 40 | * @param modifier The modifier to be applied to the slider. 41 | * @param valueRange The range of values the slider can represent. 42 | * @param gap The spacing between indicators on the slider. 43 | * @param showIndicator Determines whether to show indicators on the slider. 44 | * @param showLabel Determines whether to show a label above the slider. 45 | * @param enabled Determines whether the slider is enabled for interaction. 46 | * @param thumb The composable used to display the thumb of the slider. 47 | * @param track The composable used to display the track of the slider. 48 | * @param indicator The composable used to display the indicators on the slider. 49 | * @param label The composable used to display the label above the slider. 50 | */ 51 | @OptIn(ExperimentalMaterial3Api::class) 52 | @Composable 53 | internal fun CustomSlider( 54 | modifier: Modifier = Modifier, 55 | value: Float, 56 | currentPage:() -> Int, 57 | onValueChange: (Float) -> Unit, 58 | onValueChangeFinished: () -> Unit = {}, 59 | valueRange: ClosedFloatingPointRange = ValueRange, 60 | gap: Int = Gap, 61 | showIndicator: Boolean = false, 62 | showLabel: Boolean = false, 63 | enabled: Boolean = true, 64 | thumb: @Composable (thumbValue: Int) -> Unit = { 65 | CustomSliderDefaults.Thumb(it.toString()) 66 | }, 67 | track: @Composable (sliderState: SliderState) -> Unit = { sliderState -> 68 | CustomSliderDefaults.Track(sliderState = sliderState) 69 | }, 70 | indicator: @Composable (indicatorValue: Int) -> Unit = { indicatorValue -> 71 | CustomSliderDefaults.Indicator(indicatorValue = indicatorValue.toString()) 72 | }, 73 | label: @Composable (labelValue: Int) -> Unit = { labelValue -> 74 | CustomSliderDefaults.Label(labelValue = labelValue.toString()) 75 | } 76 | ) { 77 | val itemCount = (valueRange.endInclusive - valueRange.start).roundToInt() 78 | val steps = if (gap == 1) 0 else (itemCount / gap - 1) 79 | val verticalSliderModifier = Modifier 80 | .graphicsLayer { 81 | rotationZ = 270f 82 | transformOrigin = TransformOrigin(0F, 0F) 83 | } 84 | .layout { measurable, constraints -> 85 | val placeable = measurable.measure( 86 | Constraints( 87 | minWidth = constraints.minHeight, 88 | maxWidth = constraints.maxHeight, 89 | minHeight = constraints.minWidth, 90 | maxHeight = constraints.maxWidth 91 | ) 92 | ) 93 | layout(placeable.height,placeable.width){ 94 | placeable.place(-placeable.width,0) 95 | } 96 | } 97 | Box(modifier = modifier) { 98 | Layout( 99 | measurePolicy = customSliderMeasurePolicy( 100 | itemCount = itemCount, 101 | gap = gap, 102 | value = value, 103 | startValue = valueRange.start 104 | ), 105 | content = { 106 | if (showLabel) 107 | Label( 108 | modifier = Modifier.layoutId(CustomSliderComponents.LABEL), 109 | value = value, 110 | label = label 111 | ) 112 | 113 | Box(modifier = Modifier.layoutId(CustomSliderComponents.THUMB)) { 114 | thumb(0) 115 | } 116 | Slider( 117 | modifier = Modifier 118 | .fillMaxWidth() 119 | .layoutId(CustomSliderComponents.SLIDER), 120 | value = value, 121 | valueRange = valueRange, 122 | steps = steps, 123 | onValueChange = { onValueChange(it) }, 124 | onValueChangeFinished = onValueChangeFinished, 125 | thumb = { 126 | thumb(currentPage()) 127 | }, 128 | track = { track(it) }, 129 | enabled = enabled 130 | ) 131 | 132 | if (showIndicator) 133 | Indicator( 134 | modifier = Modifier.layoutId(CustomSliderComponents.INDICATOR), 135 | valueRange = valueRange, 136 | gap = gap, 137 | indicator = indicator 138 | ) 139 | }) 140 | } 141 | } 142 | 143 | @Composable 144 | private fun Label( 145 | modifier: Modifier = Modifier, 146 | value: Float, 147 | label: @Composable (labelValue: Int) -> Unit 148 | ) { 149 | Box( 150 | modifier = modifier, 151 | contentAlignment = Alignment.Center 152 | ) { 153 | label(value.roundToInt()) 154 | } 155 | } 156 | 157 | @Composable 158 | private fun Indicator( 159 | modifier: Modifier = Modifier, 160 | valueRange: ClosedFloatingPointRange, 161 | gap: Int, 162 | indicator: @Composable (indicatorValue: Int) -> Unit 163 | ) { 164 | // Iterate over the value range and display indicators at regular intervals. 165 | for (i in valueRange.start.roundToInt()..valueRange.endInclusive.roundToInt() step gap) { 166 | Box( 167 | modifier = modifier 168 | ) { 169 | indicator(i) 170 | } 171 | } 172 | } 173 | 174 | private fun customSliderMeasurePolicy( 175 | itemCount: Int, 176 | gap: Int, 177 | value: Float, 178 | startValue: Float 179 | ) = MeasurePolicy { measurables, constraints -> 180 | // Measure the thumb component and calculate its radius. 181 | val thumbPlaceable = measurables.first { 182 | it.layoutId == CustomSliderComponents.THUMB 183 | }.measure(constraints) 184 | val thumbRadius = (thumbPlaceable.width / 2).toFloat() 185 | 186 | val indicatorPlaceables = measurables.filter { 187 | it.layoutId == CustomSliderComponents.INDICATOR 188 | }.map { measurable -> 189 | measurable.measure(constraints) 190 | } 191 | val indicatorHeight = indicatorPlaceables.maxByOrNull { it.height }?.height ?: 0 192 | 193 | val sliderPlaceable = measurables.first { 194 | it.layoutId == CustomSliderComponents.SLIDER 195 | }.measure(constraints) 196 | val sliderHeight = sliderPlaceable.height 197 | 198 | val labelPlaceable = measurables.find { 199 | it.layoutId == CustomSliderComponents.LABEL 200 | }?.measure(constraints) 201 | val labelHeight = labelPlaceable?.height ?: 0 202 | 203 | // Calculate the total width and height of the custom slider layout 204 | val width = sliderPlaceable.width 205 | val height = labelHeight + sliderHeight + indicatorHeight 206 | 207 | // Calculate the available width for the track (excluding thumb radius on both sides). 208 | val trackWidth = width - (2 * thumbRadius) 209 | 210 | // Calculate the width of each section in the track. 211 | val sectionWidth = trackWidth / itemCount 212 | // Calculate the horizontal spacing between indicators. 213 | val indicatorSpacing = sectionWidth * gap 214 | 215 | // To calculate offset of the label, first we will calculate the progress of the slider 216 | // by subtracting startValue from the current value. 217 | // After that we will multiply this progress by the sectionWidth. 218 | // Add thumb radius to this resulting value. 219 | val labelOffset = (sectionWidth * (value - startValue)) + thumbRadius 220 | 221 | layout(width = width, height = height) { 222 | var indicatorOffsetX = thumbRadius 223 | // Place label at top. 224 | // We have to subtract the half width of the label from the labelOffset, 225 | // to place our label at the center. 226 | labelPlaceable?.placeRelative( 227 | x = (labelOffset - (labelPlaceable.width / 2)).roundToInt(), 228 | y = 0 229 | ) 230 | 231 | // Place slider placeable below the label. 232 | sliderPlaceable.placeRelative(x = 0, y = labelHeight) 233 | 234 | // Place indicators below the slider. 235 | indicatorPlaceables.forEach { placeable -> 236 | // We have to subtract the half width of the each indicator from the indicatorOffset, 237 | // to place our indicators at the center. 238 | placeable.placeRelative( 239 | x = (indicatorOffsetX - (placeable.width / 2)).roundToInt(), 240 | y = labelHeight + sliderHeight 241 | ) 242 | indicatorOffsetX += indicatorSpacing 243 | } 244 | } 245 | } 246 | 247 | /** 248 | * Object to hold defaults used by [CustomSlider] 249 | */ 250 | object CustomSliderDefaults { 251 | 252 | /** 253 | * Composable function that represents the thumb of the slider. 254 | * 255 | * @param thumbValue The value to display on the thumb. 256 | * @param modifier The modifier for styling the thumb. 257 | * @param color The color of the thumb. 258 | * @param size The size of the thumb. 259 | * @param shape The shape of the thumb. 260 | */ 261 | @Composable 262 | fun Thumb( 263 | thumbValue: String, 264 | modifier: Modifier = Modifier, 265 | color: Color = PrimaryColor, 266 | size: Dp = ThumbSize, 267 | shape: Shape = CircleShape, 268 | content: @Composable () -> Unit = { 269 | Text( 270 | text = thumbValue, 271 | color = Color.White, 272 | textAlign = TextAlign.Center 273 | ) 274 | } 275 | ) { 276 | Box( 277 | modifier = modifier 278 | .thumb(size = size, shape = shape) 279 | .background(color) 280 | .padding(2.dp), 281 | contentAlignment = Alignment.Center 282 | ) { 283 | content() 284 | } 285 | } 286 | 287 | /** 288 | * Composable function that represents the track of the slider. 289 | * 290 | * @param sliderPositions The positions of the slider. 291 | * @param modifier The modifier for styling the track. 292 | * @param trackColor The color of the track. 293 | * @param progressColor The color of the progress. 294 | * @param height The height of the track. 295 | * @param shape The shape of the track. 296 | */ 297 | @OptIn(ExperimentalMaterial3Api::class) 298 | @Composable 299 | fun Track( 300 | sliderState: SliderState, 301 | modifier: Modifier = Modifier, 302 | trackColor: Color = TrackColor, 303 | progressColor: Color = PrimaryColor, 304 | height: Dp = TrackHeight, 305 | shape: Shape = CircleShape 306 | ) { 307 | Box( 308 | modifier = modifier 309 | .track(height = height, shape = shape) 310 | .background(trackColor) 311 | ) { 312 | Box( 313 | modifier = Modifier 314 | .progress( 315 | sliderState = sliderState, 316 | height = height, 317 | shape = shape 318 | ) 319 | .background(progressColor) 320 | ) 321 | } 322 | } 323 | 324 | /** 325 | * Composable function that represents the indicator of the slider. 326 | * 327 | * @param indicatorValue The value to display as the indicator. 328 | * @param modifier The modifier for styling the indicator. 329 | * @param style The style of the indicator text. 330 | */ 331 | @Composable 332 | fun Indicator( 333 | indicatorValue: String, 334 | modifier: Modifier = Modifier, 335 | style: TextStyle = TextStyle(fontSize = 10.sp, fontWeight = FontWeight.Normal) 336 | ) { 337 | Box(modifier = modifier) { 338 | Text( 339 | text = indicatorValue, 340 | style = style, 341 | textAlign = TextAlign.Center 342 | ) 343 | } 344 | } 345 | 346 | /** 347 | * Composable function that represents the label of the slider. 348 | * 349 | * @param labelValue The value to display as the label. 350 | * @param modifier The modifier for styling the label. 351 | * @param style The style of the label text. 352 | */ 353 | @Composable 354 | fun Label( 355 | labelValue: String, 356 | modifier: Modifier = Modifier, 357 | style: TextStyle = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Normal) 358 | ) { 359 | Box(modifier = modifier) { 360 | Text( 361 | text = labelValue, 362 | style = style, 363 | textAlign = TextAlign.Center 364 | ) 365 | } 366 | } 367 | } 368 | 369 | fun Modifier.track( 370 | height: Dp = TrackHeight, 371 | shape: Shape = CircleShape 372 | ) = fillMaxWidth() 373 | .heightIn(min = height) 374 | .clip(shape) 375 | 376 | @OptIn(ExperimentalMaterial3Api::class) 377 | fun Modifier.progress( 378 | sliderState: SliderState, 379 | height: Dp = TrackHeight, 380 | shape: Shape = CircleShape 381 | ) = 382 | fillMaxWidth(fraction = sliderState.valueRange.endInclusive - sliderState.valueRange.start) 383 | .heightIn(min = height) 384 | .clip(shape) 385 | 386 | fun Modifier.thumb(size: Dp = ThumbSize, shape: Shape = CircleShape) = 387 | defaultMinSize(minWidth = size, minHeight = size).clip(shape) 388 | 389 | private enum class CustomSliderComponents { 390 | SLIDER, LABEL, INDICATOR, THUMB 391 | } 392 | 393 | val PrimaryColor = Color(0xFF6650a4) 394 | val TrackColor = Color(0xFFE7E0EC) 395 | 396 | private const val Gap = 1 397 | private val ValueRange = 0f..10f 398 | private val TrackHeight = 8.dp 399 | private val ThumbSize = 30.dp -------------------------------------------------------------------------------- /JetPDFVue/src/main/java/com/pratikk/jetpdfvue/HorizontalVueReader.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpdfvue 2 | 3 | import androidx.compose.animation.AnimatedContent 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.animation.fadeIn 6 | import androidx.compose.animation.fadeOut 7 | import androidx.compose.animation.togetherWith 8 | import androidx.compose.foundation.ExperimentalFoundationApi 9 | import androidx.compose.foundation.Image 10 | import androidx.compose.foundation.layout.PaddingValues 11 | import androidx.compose.foundation.layout.fillMaxHeight 12 | import androidx.compose.foundation.layout.fillMaxSize 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.layout.requiredWidthIn 15 | import androidx.compose.foundation.lazy.LazyRow 16 | import androidx.compose.foundation.pager.HorizontalPager 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.DisposableEffect 19 | import androidx.compose.runtime.LaunchedEffect 20 | import androidx.compose.runtime.collectAsState 21 | import androidx.compose.runtime.getValue 22 | import androidx.compose.runtime.mutableStateOf 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.runtime.setValue 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.draw.clipToBounds 27 | import androidx.compose.ui.graphics.asImageBitmap 28 | import androidx.compose.ui.layout.onSizeChanged 29 | import androidx.compose.ui.platform.LocalDensity 30 | import androidx.compose.ui.unit.IntSize 31 | import androidx.compose.ui.unit.dp 32 | import com.pratikk.jetpdfvue.state.HorizontalVueReaderState 33 | import com.pratikk.jetpdfvue.state.VuePageState 34 | import com.pratikk.jetpdfvue.util.pinchToZoomAndDrag 35 | 36 | @Composable 37 | fun HorizontalVueReader( 38 | modifier: Modifier = Modifier, 39 | contentModifier: Modifier = Modifier, 40 | horizontalVueReaderState: HorizontalVueReaderState, 41 | ) { 42 | val vueRenderer = horizontalVueReaderState.vueRenderer 43 | val currentPage = horizontalVueReaderState.currentPage 44 | if (horizontalVueReaderState.cache != 0) 45 | LaunchedEffect(key1 = currentPage, block = { 46 | vueRenderer?.loadWithCache(currentPage) 47 | }) 48 | if (vueRenderer != null) 49 | HorizontalPager( 50 | modifier = modifier, 51 | userScrollEnabled = false, 52 | state = horizontalVueReaderState.pagerState 53 | ) { idx -> 54 | val pageContent by vueRenderer.pageLists[idx].stateFlow.collectAsState() 55 | if (horizontalVueReaderState.cache == 0) 56 | DisposableEffect(key1 = Unit) { 57 | vueRenderer.pageLists[idx].load() 58 | onDispose { 59 | vueRenderer.pageLists[idx].recycle() 60 | } 61 | } 62 | AnimatedContent(targetState = pageContent, label = "") { 63 | when (it) { 64 | is VuePageState.BlankState -> { 65 | BlankPage( 66 | modifier = contentModifier, 67 | width = horizontalVueReaderState.containerSize!!.width, 68 | height = horizontalVueReaderState.containerSize!!.height 69 | ) 70 | } 71 | 72 | is VuePageState.LoadedState -> { 73 | Image( 74 | modifier = contentModifier 75 | .clipToBounds() 76 | .pinchToZoomAndDrag(), 77 | bitmap = it.content.asImageBitmap(), 78 | contentDescription = "" 79 | ) 80 | } 81 | 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /JetPDFVue/src/main/java/com/pratikk/jetpdfvue/VerticalVueReader.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpdfvue 2 | 3 | import androidx.compose.animation.AnimatedContent 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.animation.fadeIn 6 | import androidx.compose.animation.fadeOut 7 | import androidx.compose.animation.scaleIn 8 | import androidx.compose.animation.scaleOut 9 | import androidx.compose.animation.togetherWith 10 | import androidx.compose.foundation.ExperimentalFoundationApi 11 | import androidx.compose.foundation.Image 12 | import androidx.compose.foundation.layout.PaddingValues 13 | import androidx.compose.foundation.layout.fillMaxHeight 14 | import androidx.compose.foundation.layout.fillMaxWidth 15 | import androidx.compose.foundation.layout.padding 16 | import androidx.compose.foundation.layout.requiredHeightIn 17 | import androidx.compose.foundation.layout.requiredWidthIn 18 | import androidx.compose.foundation.lazy.LazyColumn 19 | import androidx.compose.foundation.pager.HorizontalPager 20 | import androidx.compose.foundation.pager.PageSize 21 | import androidx.compose.foundation.pager.VerticalPager 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.runtime.DisposableEffect 24 | import androidx.compose.runtime.LaunchedEffect 25 | import androidx.compose.runtime.collectAsState 26 | import androidx.compose.runtime.getValue 27 | import androidx.compose.runtime.mutableStateOf 28 | import androidx.compose.runtime.remember 29 | import androidx.compose.runtime.setValue 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.draw.clipToBounds 32 | import androidx.compose.ui.graphics.asImageBitmap 33 | import androidx.compose.ui.layout.onSizeChanged 34 | import androidx.compose.ui.platform.LocalDensity 35 | import androidx.compose.ui.unit.IntSize 36 | import androidx.compose.ui.unit.dp 37 | import com.pratikk.jetpdfvue.state.VerticalVueReaderState 38 | import com.pratikk.jetpdfvue.state.VuePageState 39 | import com.pratikk.jetpdfvue.util.pinchToZoomAndDrag 40 | 41 | @Composable 42 | fun VerticalVueReader( 43 | modifier: Modifier = Modifier, 44 | contentModifier: Modifier = Modifier, 45 | verticalVueReaderState: VerticalVueReaderState 46 | ) { 47 | val vueRenderer = verticalVueReaderState.vueRenderer 48 | val currentPage = verticalVueReaderState.currentPage 49 | if (verticalVueReaderState.cache != 0) 50 | LaunchedEffect(key1 = currentPage, block = { 51 | vueRenderer?.loadWithCache(currentPage) 52 | }) 53 | if (vueRenderer != null) 54 | VerticalPager( 55 | modifier = modifier, 56 | userScrollEnabled = false, 57 | state = verticalVueReaderState.pagerState 58 | ) { idx -> 59 | val pageContent by vueRenderer.pageLists[idx].stateFlow.collectAsState() 60 | if (verticalVueReaderState.cache == 0) 61 | DisposableEffect(key1 = Unit) { 62 | vueRenderer.pageLists[idx].load() 63 | onDispose { 64 | vueRenderer.pageLists[idx].recycle() 65 | } 66 | } 67 | AnimatedContent(targetState = pageContent, label = "") { 68 | when (it) { 69 | is VuePageState.BlankState -> { 70 | BlankPage( 71 | modifier = contentModifier, 72 | width = verticalVueReaderState.containerSize!!.width, 73 | height = verticalVueReaderState.containerSize!!.height 74 | ) 75 | } 76 | 77 | is VuePageState.LoadedState -> { 78 | Image( 79 | modifier = contentModifier 80 | .clipToBounds() 81 | .pinchToZoomAndDrag(), 82 | bitmap = it.content.asImageBitmap(), 83 | contentDescription = "" 84 | ) 85 | } 86 | 87 | } 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /JetPDFVue/src/main/java/com/pratikk/jetpdfvue/VueRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpdfvue 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Canvas 5 | import android.graphics.Matrix 6 | import android.graphics.pdf.PdfRenderer 7 | import android.os.ParcelFileDescriptor 8 | import androidx.compose.ui.unit.IntSize 9 | import com.pratikk.jetpdfvue.state.VuePageState 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.Job 13 | import kotlinx.coroutines.SupervisorJob 14 | import kotlinx.coroutines.cancelAndJoin 15 | import kotlinx.coroutines.flow.MutableStateFlow 16 | import kotlinx.coroutines.launch 17 | import kotlinx.coroutines.sync.Mutex 18 | import kotlinx.coroutines.sync.withLock 19 | 20 | internal class VueRenderer( 21 | private val fileDescriptor: ParcelFileDescriptor, 22 | val containerSize: IntSize, 23 | val isPortrait: Boolean, 24 | val cache:Int, 25 | ) { 26 | private val pdfRenderer = PdfRenderer(fileDescriptor) 27 | val pageCount get() = pdfRenderer.pageCount 28 | private val mutex: Mutex = Mutex() 29 | private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) 30 | val pageLists: List = List(pdfRenderer.pageCount) { 31 | Page( 32 | mutex = mutex, 33 | index = it, 34 | pdfRenderer = pdfRenderer, 35 | coroutineScope = coroutineScope, 36 | containerSize = containerSize, 37 | isPortrait = isPortrait 38 | ) 39 | } 40 | 41 | fun close() { 42 | coroutineScope.launch { 43 | pageLists.forEach { 44 | it.job?.cancelAndJoin() 45 | it.recycle() 46 | } 47 | pdfRenderer.close() 48 | fileDescriptor.close() 49 | } 50 | } 51 | 52 | fun loadWithCache(currentPage: Int) { 53 | val cacheRange = (((currentPage - cache).coerceIn(0, currentPage))..((currentPage + cache).coerceIn( 54 | currentPage, 55 | pageCount 56 | ))) 57 | 58 | pageLists.forEachIndexed { index, page -> 59 | if(cacheRange.contains(index)) 60 | page.load() 61 | else 62 | page.recycle() 63 | } 64 | } 65 | 66 | class Page( 67 | val mutex: Mutex, 68 | val index: Int, 69 | val pdfRenderer: PdfRenderer, 70 | val coroutineScope: CoroutineScope, 71 | containerSize: IntSize, 72 | isPortrait: Boolean 73 | ) { 74 | val dimension = pdfRenderer.openPage(index).use { 75 | if (isPortrait) { 76 | val h = it.height * (containerSize.width.toFloat() / it.width) 77 | val dim = Dimension( 78 | height = h.toInt(), 79 | width = containerSize.width 80 | ) 81 | dim 82 | } else { 83 | val w = it.width * (containerSize.height.toFloat() / it.height) 84 | val dim = Dimension( 85 | height = containerSize.height, 86 | width = w.toInt() 87 | ) 88 | dim 89 | } 90 | } 91 | var rotation: Float = 0F 92 | var job: Job? = null 93 | 94 | val stateFlow = MutableStateFlow( 95 | VuePageState.BlankState( 96 | width = dimension.width, 97 | height = dimension.height 98 | ) 99 | ) 100 | 101 | var isLoaded = false 102 | 103 | fun load() { 104 | if (!isLoaded) { 105 | job = coroutineScope.launch { 106 | mutex.withLock { 107 | var newBitmap: Bitmap 108 | pdfRenderer.openPage(index).use { currentPage -> 109 | newBitmap = createBlankBitmap( 110 | width = dimension.width, 111 | height = dimension.height 112 | ) 113 | currentPage.render( 114 | newBitmap, 115 | null, 116 | null, 117 | PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY 118 | ) 119 | } 120 | if (rotation != 0F) { 121 | val matrix = Matrix().apply { 122 | postRotate(rotation) 123 | } 124 | val rotatedBitmap = Bitmap.createBitmap( 125 | newBitmap, 126 | 0, 127 | 0, 128 | newBitmap.width, 129 | newBitmap.height, 130 | matrix, 131 | true 132 | ) 133 | newBitmap.recycle() 134 | isLoaded = true 135 | stateFlow.emit(VuePageState.LoadedState(rotatedBitmap)) 136 | } else { 137 | isLoaded = true 138 | stateFlow.emit(VuePageState.LoadedState(newBitmap)) 139 | } 140 | } 141 | 142 | } 143 | } 144 | } 145 | 146 | fun recycle() { 147 | isLoaded = false 148 | val oldBitmap = stateFlow.value as? VuePageState.LoadedState 149 | stateFlow.tryEmit( 150 | VuePageState.BlankState( 151 | width = dimension.width, 152 | height = dimension.height 153 | ) 154 | ) 155 | oldBitmap?.content?.recycle() 156 | } 157 | 158 | fun refresh() { 159 | stateFlow.tryEmit( 160 | VuePageState.BlankState( 161 | width = dimension.width, 162 | height = dimension.height 163 | ) 164 | ) 165 | isLoaded = false 166 | val oldBitmap = stateFlow.value as? VuePageState.LoadedState 167 | oldBitmap?.content?.recycle() 168 | load() 169 | } 170 | 171 | private fun createBlankBitmap( 172 | width: Int, 173 | height: Int 174 | ): Bitmap { 175 | return Bitmap.createBitmap( 176 | width, 177 | height, 178 | Bitmap.Config.ARGB_8888 179 | ).apply { 180 | val canvas = Canvas(this) 181 | canvas.drawColor(android.graphics.Color.WHITE) 182 | canvas.drawBitmap(this, 0f, 0f, null) 183 | } 184 | } 185 | 186 | data class Dimension( 187 | val height: Int, 188 | val width: Int 189 | ) 190 | } 191 | } -------------------------------------------------------------------------------- /JetPDFVue/src/main/java/com/pratikk/jetpdfvue/VueSlider.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpdfvue 2 | 3 | import androidx.compose.material3.ExperimentalMaterial3Api 4 | import androidx.compose.material3.SliderPositions 5 | import androidx.compose.material3.SliderState 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.LaunchedEffect 8 | import androidx.compose.runtime.derivedStateOf 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.runtime.saveable.rememberSaveable 13 | import androidx.compose.runtime.setValue 14 | import androidx.compose.ui.Modifier 15 | import com.pratikk.jetpdfvue.state.HorizontalVueReaderState 16 | 17 | @OptIn(ExperimentalMaterial3Api::class) 18 | @Composable 19 | fun VueHorizontalSlider( 20 | modifier: Modifier = Modifier, 21 | horizontalVueReaderState: HorizontalVueReaderState, 22 | onValueChange: (Float) -> Unit = {}, 23 | onValueChangeFinished: () -> Unit = {}, 24 | gap: Int = 1, 25 | showIndicator: Boolean = false, 26 | showLabel: Boolean = false, 27 | enabled: Boolean = true, 28 | thumb: @Composable (thumbValue: Int) -> Unit = { 29 | CustomSliderDefaults.Thumb(it.toString()) 30 | }, 31 | track: @Composable (sliderState: SliderState) -> Unit = { sliderState -> 32 | CustomSliderDefaults.Track(sliderState = sliderState) 33 | }, 34 | indicator: @Composable (indicatorValue: Int) -> Unit = { indicatorValue -> 35 | CustomSliderDefaults.Indicator(indicatorValue = indicatorValue.toString()) 36 | }, 37 | label: @Composable (labelValue: Int) -> Unit = { labelValue -> 38 | CustomSliderDefaults.Label(labelValue = labelValue.toString()) 39 | }, 40 | ) { 41 | var sliderState by rememberSaveable { 42 | mutableStateOf(0f) 43 | } 44 | val moveToPage by remember(sliderState) { 45 | derivedStateOf { 46 | sliderState.toInt() 47 | } 48 | } 49 | LaunchedEffect(key1 = moveToPage, block = { 50 | horizontalVueReaderState.pagerState.animateScrollToPage(moveToPage) 51 | }) 52 | 53 | CustomSlider( 54 | modifier = modifier, 55 | currentPage = { horizontalVueReaderState.currentPage }, 56 | value = sliderState, 57 | onValueChange = { 58 | sliderState = it 59 | onValueChange(it) 60 | }, 61 | gap = gap, 62 | showIndicator = showIndicator, 63 | showLabel = showLabel, 64 | enabled = enabled, 65 | onValueChangeFinished = onValueChangeFinished, 66 | valueRange = 0F..horizontalVueReaderState.pdfPageCount.toFloat(), 67 | thumb = thumb, 68 | track = track, 69 | indicator = indicator, 70 | label = label 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /JetPDFVue/src/main/java/com/pratikk/jetpdfvue/network/VueDownloader.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpdfvue.network 2 | 3 | import android.util.Base64 4 | import android.util.Log 5 | import com.pratikk.jetpdfvue.state.VueFileType 6 | import com.pratikk.jetpdfvue.state.VueResourceType 7 | import com.pratikk.jetpdfvue.util.addImageToPdf 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.withContext 10 | import java.io.BufferedInputStream 11 | import java.io.ByteArrayOutputStream 12 | import java.io.File 13 | import java.io.FileNotFoundException 14 | import java.io.FileOutputStream 15 | import java.net.HttpURLConnection 16 | import java.net.URL 17 | import java.net.UnknownHostException 18 | 19 | suspend fun vueDownload( 20 | vueResource: VueResourceType.Remote, 21 | file: File, 22 | onProgressChange: (Int) -> Unit, 23 | onSuccess: () -> Unit, 24 | onError: (Exception) -> Unit 25 | ) { 26 | withContext(Dispatchers.IO){ 27 | try { 28 | // URL of the file you want to download 29 | val fileUrl = vueResource.url 30 | 31 | // Create a URL object 32 | val url = URL(fileUrl) 33 | 34 | // Open a connection to the URL 35 | val connection = url.openConnection() as HttpURLConnection 36 | 37 | // Set request method (GET is used for downloading files) 38 | connection.requestMethod = "GET" 39 | 40 | // Set custom headers if needed 41 | vueResource.headers.forEach { 42 | connection.setRequestProperty(it.key, it.value) 43 | } 44 | 45 | // Get the input stream from the connection 46 | val inputStream = connection.inputStream 47 | 48 | // Create a BufferedInputStream for efficient reading 49 | val bufferedInputStream = BufferedInputStream(inputStream) 50 | 51 | //Write to file when resource type is pdf 52 | val outputStream = FileOutputStream(file.absolutePath) 53 | 54 | //Write to temporary byte array when resource type is base64 55 | val byteArrayOutputStream = ByteArrayOutputStream() 56 | 57 | // Get the content length (file size) from the response headers 58 | val contentLength = connection.getHeaderField("Content-Length") 59 | val contentType = connection.getHeaderField("Content-Type") 60 | val fileSize = contentLength?.toLong() ?: throw Exception("File not found on server") 61 | // Read and write the file data 62 | val buffer = ByteArray(1024) 63 | var bytesRead: Int 64 | var totalBytesRead = 0 65 | when(vueResource.fileType){ 66 | VueFileType.PDF -> { 67 | while (bufferedInputStream.read(buffer).also { bytesRead = it } != -1) { 68 | outputStream.write(buffer, 0, bytesRead) 69 | totalBytesRead += bytesRead 70 | onProgressChange((totalBytesRead.toDouble() / fileSize.toDouble() * 100).toInt()) 71 | } 72 | // Close the streams 73 | outputStream.close() 74 | bufferedInputStream.close() 75 | onSuccess() 76 | } 77 | VueFileType.IMAGE -> { 78 | val imgFile = File.createTempFile("imageTemp",contentType.split("/")[1]) 79 | val imgOutputStream = FileOutputStream(imgFile) 80 | while (bufferedInputStream.read(buffer).also { bytesRead = it } != -1) { 81 | imgOutputStream.write(buffer, 0, bytesRead) 82 | totalBytesRead += bytesRead 83 | onProgressChange((totalBytesRead.toDouble() / fileSize.toDouble() * 100).toInt()) 84 | } 85 | imgOutputStream.close() 86 | addImageToPdf(imageFilePath = imgFile.absolutePath, pdfPath = file.absolutePath) 87 | 88 | // Close the streams 89 | outputStream.close() 90 | bufferedInputStream.close() 91 | onSuccess() 92 | } 93 | VueFileType.BASE64 -> { 94 | while (bufferedInputStream.read(buffer).also { bytesRead = it } != -1) { 95 | byteArrayOutputStream.write(buffer, 0, bytesRead) 96 | totalBytesRead += bytesRead 97 | onProgressChange((totalBytesRead.toDouble() / fileSize.toDouble() * 100).toInt()) 98 | } 99 | 100 | val decodedByteStream = Base64.decode(byteArrayOutputStream.toByteArray(),Base64.DEFAULT) 101 | outputStream.write(decodedByteStream) 102 | 103 | // Close the streams 104 | outputStream.close() 105 | bufferedInputStream.close() 106 | onSuccess() 107 | } 108 | } 109 | }catch (e: FileNotFoundException) { 110 | file.delete() 111 | onError(Exception("File not found")) 112 | } catch (e: UnknownHostException) { 113 | file.delete() 114 | onError(Exception("No internet connection")) 115 | } catch (e: Exception) { 116 | onError(e) 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /JetPDFVue/src/main/java/com/pratikk/jetpdfvue/state/HorizontalVueReaderState.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpdfvue.state 2 | 3 | import android.content.Context 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.saveable.Saver 6 | import androidx.compose.runtime.saveable.listSaver 7 | import androidx.compose.runtime.saveable.rememberSaveable 8 | import androidx.compose.ui.unit.IntSize 9 | import androidx.core.net.toUri 10 | import kotlinx.coroutines.CoroutineScope 11 | import java.io.File 12 | 13 | class HorizontalVueReaderState( 14 | resource: VueResourceType 15 | ) : VueReaderState(resource) { 16 | internal var pagerState = VuePagerState( 17 | initialPage = 0, 18 | initialPageOffsetFraction = 0f, 19 | updatedPageCount = { pdfPageCount }) 20 | 21 | override suspend fun nextPage() { 22 | pagerState.animateScrollToPage(pagerState.currentPage + 1) 23 | } 24 | 25 | override suspend fun prevPage() { 26 | pagerState.animateScrollToPage(pagerState.currentPage - 1) 27 | } 28 | 29 | override fun rotate(angle: Float) { 30 | vueRenderer?.pageLists?.get(pagerState.currentPage)?.apply { 31 | rotation += angle % 360F 32 | refresh() 33 | } 34 | } 35 | 36 | override val currentPage: Int 37 | get() = pagerState.currentPage + 1 38 | override val isScrolling: Boolean 39 | get() = pagerState.isScrollInProgress 40 | override val TAG: String 41 | get() = "HorizontalVueReader" 42 | 43 | 44 | override fun load( 45 | context: Context, 46 | coroutineScope: CoroutineScope, 47 | containerSize: IntSize, 48 | isPortrait: Boolean, 49 | customResource: (suspend CoroutineScope.() -> File)? 50 | ) { 51 | this.containerSize = containerSize 52 | this.isPortrait = isPortrait 53 | loadResource( 54 | context = context, 55 | coroutineScope = coroutineScope, 56 | loadCustomResource = customResource 57 | ) 58 | } 59 | 60 | companion object { 61 | val Saver: Saver = listSaver( 62 | save = { 63 | it.importJob?.cancel() 64 | val resource = 65 | it.file?.let { file -> 66 | if (it.vueResource is VueResourceType.BlankDocument && !it.isDocumentModified) 67 | VueResourceType.BlankDocument(file.toUri()) 68 | else 69 | VueResourceType.Local( 70 | file.toUri(), 71 | it.getResourceType() 72 | ) 73 | } ?: it.vueResource 74 | 75 | buildList { 76 | add(resource) 77 | add(it.importFile?.absolutePath) 78 | add(it.pagerState.currentPage) 79 | if (it.vueLoadState is VueLoadState.DocumentImporting) 80 | add(it.vueLoadState) 81 | else 82 | add(VueLoadState.DocumentLoading) 83 | add(it.vueImportState) 84 | add(it.mDocumentModified) 85 | add(it.cache) 86 | }.toList() 87 | }, 88 | restore = { 89 | HorizontalVueReaderState(it[0] as VueResourceType).apply { 90 | //Restore file path 91 | importFile = if (it[1] != null) File(it[1] as String) else null 92 | //Restore Pager State 93 | pagerState = VuePagerState( 94 | initialPage = it[2] as Int, 95 | initialPageOffsetFraction = 0F, 96 | updatedPageCount = { pdfPageCount }) 97 | //Restoring in case it was in importing state 98 | vueLoadState = it[3] as VueLoadState 99 | //To resume importing on configuration change 100 | vueImportState = it[4] as VueImportState 101 | //Restore document modified flag 102 | mDocumentModified = it[5] as Boolean 103 | //Restore cache value 104 | cache = it[6] as Int 105 | } 106 | } 107 | ) 108 | } 109 | } 110 | 111 | @Composable 112 | fun rememberHorizontalVueReaderState( 113 | resource: VueResourceType, 114 | cache: Int = 0 115 | ): HorizontalVueReaderState { 116 | return rememberSaveable(saver = HorizontalVueReaderState.Saver) { 117 | HorizontalVueReaderState(resource).apply { this.cache = cache } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /JetPDFVue/src/main/java/com/pratikk/jetpdfvue/state/PagerState.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpdfvue.state 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.pager.PagerState 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.saveable.Saver 7 | import androidx.compose.runtime.saveable.listSaver 8 | 9 | @OptIn(ExperimentalFoundationApi::class) 10 | internal class VuePagerState( 11 | initialPage: Int, 12 | initialPageOffsetFraction: Float, 13 | updatedPageCount: () -> Int 14 | ) : PagerState(initialPage, initialPageOffsetFraction) { 15 | 16 | var pageCountState = mutableStateOf(updatedPageCount) 17 | override val pageCount: Int get() = pageCountState.value.invoke() 18 | 19 | companion object { 20 | /** 21 | * To keep current page and current page offset saved 22 | */ 23 | val Saver: Saver = listSaver( 24 | save = { 25 | listOf( 26 | it.currentPage, 27 | it.currentPageOffsetFraction, 28 | it.pageCount 29 | ) 30 | }, 31 | restore = { 32 | VuePagerState( 33 | initialPage = it[0] as Int, 34 | initialPageOffsetFraction = it[1] as Float, 35 | updatedPageCount = { it[2] as Int } 36 | ) 37 | } 38 | ) 39 | } 40 | } -------------------------------------------------------------------------------- /JetPDFVue/src/main/java/com/pratikk/jetpdfvue/state/VerticalVueReaderState.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpdfvue.state 2 | 3 | import android.content.Context 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.saveable.Saver 6 | import androidx.compose.runtime.saveable.listSaver 7 | import androidx.compose.runtime.saveable.rememberSaveable 8 | import androidx.compose.ui.unit.IntSize 9 | import androidx.core.net.toUri 10 | import kotlinx.coroutines.CoroutineScope 11 | import java.io.File 12 | 13 | class VerticalVueReaderState( 14 | resource: VueResourceType 15 | ) : VueReaderState(resource) { 16 | 17 | internal var pagerState = VuePagerState( 18 | initialPage = 0, 19 | initialPageOffsetFraction = 0f, 20 | updatedPageCount = { pdfPageCount }) 21 | 22 | override suspend fun nextPage() { 23 | pagerState.animateScrollToPage(pagerState.currentPage + 1) 24 | } 25 | 26 | override suspend fun prevPage() { 27 | pagerState.animateScrollToPage(pagerState.currentPage - 1) 28 | } 29 | 30 | override fun rotate(angle: Float) { 31 | vueRenderer?.pageLists?.get(pagerState.currentPage)?.apply { 32 | rotation += angle % 360F 33 | refresh() 34 | } 35 | } 36 | 37 | override val currentPage: Int 38 | get() = pagerState.currentPage + 1 39 | override val isScrolling: Boolean 40 | get() = pagerState.isScrollInProgress 41 | 42 | override val TAG: String 43 | get() = "HorizontalVueReader" 44 | 45 | 46 | override fun load( 47 | context: Context, 48 | coroutineScope: CoroutineScope, 49 | containerSize: IntSize, 50 | isPortrait: Boolean, 51 | customResource: (suspend CoroutineScope.() -> File)? 52 | ) { 53 | this.containerSize = containerSize 54 | this.isPortrait = isPortrait 55 | loadResource( 56 | context = context, 57 | coroutineScope = coroutineScope, 58 | loadCustomResource = customResource 59 | ) 60 | } 61 | 62 | companion object { 63 | val Saver: Saver = listSaver( 64 | save = { 65 | it.importJob?.cancel() 66 | val resource = 67 | it.file?.let { file -> 68 | if (it.vueResource is VueResourceType.BlankDocument && !it.isDocumentModified) 69 | VueResourceType.BlankDocument(file.toUri()) 70 | else 71 | VueResourceType.Local( 72 | file.toUri() 73 | ) 74 | } ?: it.vueResource 75 | 76 | buildList { 77 | add(resource) 78 | add(it.importFile?.absolutePath) 79 | add(it.pagerState.currentPage) 80 | if (it.vueLoadState is VueLoadState.DocumentImporting) 81 | add(it.vueLoadState) 82 | else 83 | add(VueLoadState.DocumentLoading) 84 | add(it.vueImportState) 85 | add(it.mDocumentModified) 86 | add(it.cache) 87 | }.toList() 88 | }, 89 | restore = { 90 | VerticalVueReaderState(it[0] as VueResourceType).apply { 91 | //Restore file path 92 | importFile = if (it[1] != null) File(it[1] as String) else null 93 | //Restore list state 94 | pagerState = VuePagerState( 95 | initialPage = it[2] as Int, 96 | initialPageOffsetFraction = 0F, 97 | updatedPageCount = { pdfPageCount }) 98 | //Restoring in case it was in importing state 99 | vueLoadState = it[3] as VueLoadState 100 | //To resume importing on configuration change 101 | vueImportState = it[4] as VueImportState 102 | //Restore document modified flag 103 | mDocumentModified = it[5] as Boolean 104 | //Restore cache value 105 | cache = it[6] as Int 106 | } 107 | } 108 | ) 109 | } 110 | } 111 | 112 | @Composable 113 | fun rememberVerticalVueReaderState( 114 | resource: VueResourceType, 115 | cache: Int = 0 116 | ): VerticalVueReaderState { 117 | return rememberSaveable(saver = VerticalVueReaderState.Saver) { 118 | VerticalVueReaderState(resource).apply { this.cache = cache } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /JetPDFVue/src/main/java/com/pratikk/jetpdfvue/state/VueFilePicker.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpdfvue.state 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.os.Build 8 | import android.os.Parcelable 9 | import android.provider.MediaStore 10 | import androidx.activity.compose.ManagedActivityResultLauncher 11 | import androidx.activity.compose.rememberLauncherForActivityResult 12 | import androidx.activity.result.ActivityResult 13 | import androidx.activity.result.contract.ActivityResultContracts 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.LaunchedEffect 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.saveable.Saver 19 | import androidx.compose.runtime.saveable.listSaver 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.platform.LocalContext 22 | import androidx.core.content.FileProvider 23 | import androidx.core.net.toUri 24 | import com.pratikk.jetpdfvue.util.getDateddMMyyyyHHmm 25 | import com.pratikk.jetpdfvue.util.getFileType 26 | import com.pratikk.jetpdfvue.util.toFile 27 | import kotlinx.coroutines.CoroutineStart 28 | import kotlinx.coroutines.Dispatchers 29 | import kotlinx.coroutines.Job 30 | import kotlinx.coroutines.isActive 31 | import kotlinx.coroutines.launch 32 | import kotlinx.parcelize.Parcelize 33 | import java.io.File 34 | import java.util.Calendar 35 | 36 | sealed class VueFilePickerState { 37 | @Parcelize 38 | data class VueFilePickerImported(val uri: Uri) : VueFilePickerState(), Parcelable 39 | 40 | @Parcelize 41 | data object VueFilePickerIdeal : VueFilePickerState(), Parcelable 42 | } 43 | 44 | enum class VueImportSources { 45 | CAMERA, GALLERY, BASE64, PDF 46 | } 47 | 48 | class VueFilePicker { 49 | private var importFile: File? = null 50 | private var importJob: Job? = null 51 | var isImporting by mutableStateOf(false) 52 | private set 53 | private var vueFilePickerState by mutableStateOf(VueFilePickerState.VueFilePickerIdeal) 54 | 55 | companion object { 56 | val Saver: Saver = listSaver( 57 | save = { 58 | it.importJob?.cancel() 59 | buildList { 60 | add(it.importFile?.absolutePath) 61 | add(it.vueFilePickerState) 62 | }.toList() 63 | }, 64 | restore = { 65 | VueFilePicker().apply { 66 | importFile = (it[0] as String?)?.let { path -> 67 | File(path) 68 | } 69 | vueFilePickerState = it[1] as VueFilePickerState 70 | } 71 | } 72 | ) 73 | val UriSaver:Saver = listSaver( 74 | save = { listOf(VueFilePickerState.VueFilePickerImported(it)) }, 75 | restore = { 76 | it[0].uri 77 | } 78 | ) 79 | } 80 | 81 | /** 82 | * @param vueImportSources At least one source is required. If base64 and pdf both are included then 83 | * the file manager will enable importing of other file types as well. 84 | * */ 85 | fun launchIntent( 86 | context: Context, 87 | vueImportSources: List, 88 | launcher: ManagedActivityResultLauncher 89 | ) { 90 | require( 91 | value = vueImportSources.isNotEmpty(), 92 | lazyMessage = { "File Sources cannot be empty" }) 93 | val intents = ArrayList() 94 | val filterImportState = vueImportSources.toMutableList().let { 95 | if (it.contains(VueImportSources.BASE64) && it.contains(VueImportSources.PDF) && it.contains(VueImportSources.GALLERY)) { 96 | it.remove(VueImportSources.PDF) 97 | it.remove(VueImportSources.BASE64) 98 | intents.add(base64PdfAndGalleryIntent()) 99 | }else if(it.contains(VueImportSources.BASE64) && it.contains(VueImportSources.PDF)){ 100 | it.remove(VueImportSources.PDF) 101 | it.remove(VueImportSources.BASE64) 102 | intents.add(base64AndPdfIntent()) 103 | }else if(it.contains(VueImportSources.PDF) && it.contains(VueImportSources.GALLERY)) { 104 | it.remove(VueImportSources.PDF) 105 | intents.add(pdfAndGalleryIntent()) 106 | } 107 | it 108 | } 109 | filterImportState.forEach { source -> 110 | val intent = when (source) { 111 | VueImportSources.CAMERA -> cameraIntent(context) 112 | VueImportSources.GALLERY -> galleryIntent() 113 | VueImportSources.BASE64 -> base64Intent() 114 | VueImportSources.PDF -> pdfIntent() 115 | } 116 | intents.add(intent) 117 | } 118 | val chooserIntent = createChooserIntent(intents) 119 | launcher.launch(chooserIntent) 120 | } 121 | 122 | /** 123 | * @param interceptResult Any operations to be done on file should happen inside this lambda. Note : This lambda will be invoked on every configuration change until onResult is called 124 | * @param onResult Final result can be obtained inside this block. The result should be copied to a known location for further use 125 | */ 126 | @Composable 127 | fun getLauncher( 128 | interceptResult: suspend (File) -> Unit = {}, 129 | onResult: (File) -> Unit = {} 130 | ): ManagedActivityResultLauncher { 131 | val context = LocalContext.current 132 | LaunchedEffect(key1 = vueFilePickerState, block = { 133 | if (vueFilePickerState is VueFilePickerState.VueFilePickerImported && importJob == null) { 134 | importJob = launch(context = coroutineContext + Dispatchers.IO, start = CoroutineStart.LAZY) { 135 | with((vueFilePickerState as VueFilePickerState.VueFilePickerImported)) { 136 | //Create a temp file using result uri 137 | val file = context.contentResolver.openInputStream(uri)?.use { 138 | val ext = when (uri.getFileType(context)) { 139 | VueFileType.PDF -> { 140 | "pdf" 141 | } 142 | 143 | VueFileType.IMAGE -> { 144 | "jpg" 145 | } 146 | 147 | VueFileType.BASE64 -> { 148 | "txt" 149 | } 150 | } 151 | it.toFile(ext) 152 | }!! 153 | interceptResult(file) 154 | 155 | if (isActive) { 156 | onResult(file) 157 | } 158 | } 159 | }.apply { 160 | invokeOnCompletion { 161 | importJob = null 162 | vueFilePickerState = VueFilePickerState.VueFilePickerIdeal 163 | isImporting = false 164 | } 165 | } 166 | importJob?.start() 167 | isImporting = true 168 | importJob?.join() 169 | } 170 | }) 171 | return rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { 172 | vueFilePickerState = if (it.resultCode == Activity.RESULT_OK) { 173 | val uri = it.data?.data 174 | if (uri != null) { 175 | //Other sources 176 | VueFilePickerState.VueFilePickerImported(uri) 177 | } else { 178 | //From Camera 179 | VueFilePickerState.VueFilePickerImported(importFile!!.toUri()) 180 | } 181 | } else { 182 | VueFilePickerState.VueFilePickerIdeal 183 | } 184 | } 185 | } 186 | 187 | private fun createChooserIntent(intents: ArrayList): Intent { 188 | val chooserIntent = Intent.createChooser(Intent(), "Select action") 189 | chooserIntent.putExtra(Intent.EXTRA_INTENT, intents[0]) 190 | if (intents.size > 1) { 191 | intents.removeAt(0) 192 | chooserIntent.putExtra( 193 | Intent.EXTRA_INITIAL_INTENTS, 194 | intents.toTypedArray() 195 | ) 196 | } 197 | return chooserIntent 198 | } 199 | 200 | private fun cameraIntent(context: Context): Intent { 201 | importFile = File( 202 | context.filesDir, 203 | "${Calendar.getInstance().timeInMillis.getDateddMMyyyyHHmm()}_${Calendar.getInstance().timeInMillis}.jpg" 204 | ) 205 | if (importFile?.parentFile?.exists() == false) { 206 | importFile?.parentFile?.mkdirs() 207 | } 208 | val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { 209 | val uri: Uri = if (Build.VERSION.SDK_INT < 24) 210 | Uri.fromFile(importFile) 211 | else 212 | FileProvider.getUriForFile( 213 | context, 214 | context.applicationContext.packageName + ".provider", 215 | importFile!! 216 | ) 217 | putExtra(MediaStore.EXTRA_OUTPUT, uri) 218 | } 219 | return cameraIntent 220 | } 221 | 222 | private fun galleryIntent(): Intent { 223 | val intent = Intent(Intent.ACTION_PICK) 224 | intent.type = "image/*" 225 | return intent 226 | } 227 | 228 | private fun base64Intent(): Intent { 229 | val intent = Intent(Intent.ACTION_GET_CONTENT) 230 | intent.type = "text/plain" 231 | intent.addCategory(Intent.CATEGORY_OPENABLE) 232 | return intent 233 | } 234 | 235 | private fun pdfIntent(): Intent { 236 | val intent = Intent(Intent.ACTION_GET_CONTENT) 237 | intent.type = "application/pdf" 238 | intent.addCategory(Intent.CATEGORY_OPENABLE) 239 | return intent 240 | } 241 | 242 | private fun base64PdfAndGalleryIntent(): Intent { 243 | val intent = Intent(Intent.ACTION_GET_CONTENT) 244 | intent.type = "*/*" 245 | intent.putExtra( 246 | Intent.EXTRA_MIME_TYPES, 247 | listOf("application/pdf", "text/plain","image/*").toTypedArray() 248 | ) 249 | intent.addCategory(Intent.CATEGORY_OPENABLE) 250 | return intent 251 | } 252 | 253 | private fun base64AndPdfIntent(): Intent { 254 | val intent = Intent(Intent.ACTION_GET_CONTENT) 255 | intent.type = "*/*" 256 | intent.putExtra( 257 | Intent.EXTRA_MIME_TYPES, 258 | listOf("application/pdf", "text/plain").toTypedArray() 259 | ) 260 | intent.addCategory(Intent.CATEGORY_OPENABLE) 261 | return intent 262 | } 263 | private fun pdfAndGalleryIntent(): Intent { 264 | val intent = Intent(Intent.ACTION_GET_CONTENT) 265 | intent.type = "*/*" 266 | intent.putExtra( 267 | Intent.EXTRA_MIME_TYPES, 268 | listOf("application/pdf", "image/*").toTypedArray() 269 | ) 270 | intent.addCategory(Intent.CATEGORY_OPENABLE) 271 | return intent 272 | } 273 | } -------------------------------------------------------------------------------- /JetPDFVue/src/main/java/com/pratikk/jetpdfvue/state/VueImportState.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpdfvue.state 2 | 3 | import android.net.Uri 4 | import android.os.Parcelable 5 | import kotlinx.parcelize.Parcelize 6 | 7 | sealed class VueImportState{ 8 | 9 | @Parcelize 10 | data class Imported(val uri: Uri?): VueImportState(), Parcelable 11 | @Parcelize 12 | data class Ideal(private val ideal:Boolean = true) : VueImportState(), Parcelable 13 | } -------------------------------------------------------------------------------- /JetPDFVue/src/main/java/com/pratikk/jetpdfvue/state/VueLoadState.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpdfvue.state 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | sealed class VueLoadState{ 7 | val getErrorMessage 8 | get() = if(this is DocumentError) error?.message.toString() else null 9 | @Parcelize 10 | object NoDocument : VueLoadState(),Parcelable 11 | 12 | @Parcelize 13 | object DocumentLoading : VueLoadState(),Parcelable 14 | @Parcelize 15 | object DocumentImporting : VueLoadState(),Parcelable 16 | @Parcelize 17 | object DocumentLoaded : VueLoadState(),Parcelable 18 | @Parcelize 19 | data class DocumentError(val error: Throwable?) : VueLoadState(),Parcelable 20 | } -------------------------------------------------------------------------------- /JetPDFVue/src/main/java/com/pratikk/jetpdfvue/state/VuePageState.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpdfvue.state 2 | 3 | import android.graphics.Bitmap 4 | 5 | sealed interface VuePageState{ 6 | data class LoadedState( 7 | val content: Bitmap 8 | ) : VuePageState 9 | 10 | data class BlankState( 11 | val width: Int, 12 | val height: Int 13 | ) : VuePageState 14 | } -------------------------------------------------------------------------------- /JetPDFVue/src/main/java/com/pratikk/jetpdfvue/state/VueResourceType.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpdfvue.state 2 | 3 | import android.net.Uri 4 | import android.os.Parcelable 5 | import androidx.annotation.RawRes 6 | import kotlinx.parcelize.Parcelize 7 | 8 | /** 9 | * Enum class to indicate which file is provided with VueResourceType 10 | * @see VueResourceType 11 | */ 12 | @Parcelize 13 | enum class VueFileType:Parcelable{ 14 | PDF,IMAGE,BASE64 15 | } 16 | sealed class VueResourceType{ 17 | 18 | /** 19 | * @param uri If null then internally an empty file would be create otherwise param uri will be used as file 20 | * */ 21 | @Parcelize 22 | data class BlankDocument(val uri:Uri? = null): VueResourceType(), Parcelable 23 | 24 | /** 25 | * @param uri Source file uri 26 | * @param fileType Source file type 27 | * @see VueFileType 28 | */ 29 | @Parcelize 30 | data class Local(val uri: Uri,val fileType:VueFileType = VueFileType.PDF) : VueResourceType(), Parcelable 31 | 32 | /** 33 | * @param url Source file url (Method type GET) 34 | * @param fileType Source file type 35 | * @see VueFileType 36 | * @param headers Headers if required when fetching from url 37 | */ 38 | @Parcelize 39 | data class Remote( 40 | val url: String, 41 | val headers: HashMap = hashMapOf(), 42 | val fileType:VueFileType 43 | ) : VueResourceType(), Parcelable 44 | 45 | /** 46 | * @param assetId Source asset id 47 | * @param fileType Source file type 48 | * @see VueFileType 49 | */ 50 | @Parcelize 51 | data class Asset(@RawRes val assetId: Int,val fileType:VueFileType) : VueResourceType(), Parcelable 52 | 53 | @Parcelize 54 | data object Custom : VueResourceType(), Parcelable 55 | } -------------------------------------------------------------------------------- /JetPDFVue/src/main/java/com/pratikk/jetpdfvue/util/PDFUtil.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpdfvue.util 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.BitmapFactory 5 | import android.graphics.pdf.PdfDocument 6 | import android.graphics.pdf.PdfRenderer 7 | import android.os.ParcelFileDescriptor 8 | import androidx.compose.foundation.ExperimentalFoundationApi 9 | import androidx.compose.foundation.combinedClickable 10 | import androidx.compose.foundation.gestures.detectTransformGestures 11 | import androidx.compose.foundation.interaction.MutableInteractionSource 12 | import androidx.compose.foundation.layout.offset 13 | import androidx.compose.runtime.DisposableEffect 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.composed 20 | import androidx.compose.ui.graphics.graphicsLayer 21 | import androidx.compose.ui.input.pointer.pointerInput 22 | import androidx.compose.ui.platform.LocalConfiguration 23 | import androidx.compose.ui.unit.IntOffset 24 | import androidx.compose.ui.unit.dp 25 | import kotlinx.coroutines.Dispatchers 26 | import kotlinx.coroutines.isActive 27 | import kotlinx.coroutines.withContext 28 | import kotlinx.coroutines.yield 29 | import java.io.File 30 | import java.io.FileOutputStream 31 | import kotlin.math.cos 32 | import kotlin.math.roundToInt 33 | import kotlin.math.sin 34 | 35 | /** 36 | * Util function to add page to pdf 37 | * page can be a file or a bitmap 38 | * */ 39 | internal suspend fun addImageToPdf( 40 | imageFilePath: String? = null, 41 | bitmap: Bitmap? = null, 42 | pdfPath: String 43 | ) { 44 | withContext(Dispatchers.IO) { 45 | if (imageFilePath.isNullOrEmpty() && bitmap == null) 46 | throw Exception("Image file or bitmap required") 47 | val pdfFile = File.createTempFile("temp", ".pdf") 48 | val actualFile = File(pdfPath) 49 | if(actualFile.exists() && actualFile.length() != 0L) 50 | actualFile.copyTo(pdfFile, true) 51 | val pdfDocument = PdfDocument() 52 | val options = BitmapFactory.Options() 53 | val image = if (!imageFilePath.isNullOrEmpty()) BitmapFactory.decodeFile( 54 | imageFilePath, 55 | options 56 | ) else bitmap!! 57 | 58 | if (!actualFile.exists() || actualFile.length() == 0L){ 59 | val pageInfo = PdfDocument.PageInfo.Builder(image.width, image.height, 1).create() 60 | val page = pdfDocument.startPage(pageInfo) 61 | val canvas = page.canvas 62 | canvas.drawBitmap(image, 0f, 0f, null) 63 | pdfDocument.finishPage(page) 64 | // Save the changes to the existing or new PDF document 65 | val outputStream = FileOutputStream(pdfFile) 66 | pdfDocument.writeTo(outputStream) 67 | pdfDocument.close() 68 | image.recycle() 69 | if (isActive) { 70 | pdfFile.copyTo(actualFile, true) 71 | } 72 | return@withContext 73 | } 74 | val mrenderer = PdfRenderer( 75 | ParcelFileDescriptor.open( 76 | pdfFile, 77 | ParcelFileDescriptor.MODE_READ_ONLY 78 | ) 79 | ) 80 | 81 | for (i in 0 until mrenderer.pageCount) { 82 | val originalPage = mrenderer.openPage(i) 83 | 84 | // Create a new bitmap to draw the contents of the original page onto 85 | val pageBitmap = Bitmap.createBitmap( 86 | originalPage.width, 87 | originalPage.height, 88 | Bitmap.Config.ARGB_8888 89 | ) 90 | // Draw the contents of the original page onto the pageBitmap 91 | originalPage.render(pageBitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_PRINT) 92 | // Close the original page 93 | originalPage.close() 94 | 95 | //Create new page for pageBitmap 96 | val pageInfo = 97 | PdfDocument.PageInfo.Builder(pageBitmap.width, pageBitmap.height, i + 1) 98 | .create() 99 | val currentPage = pdfDocument.startPage(pageInfo) 100 | 101 | val mCanvas = currentPage.canvas 102 | 103 | 104 | // Draw the pageBitmap onto the canvas of the existing page 105 | mCanvas.drawBitmap(pageBitmap, 0f, 0f, null) 106 | pageBitmap.recycle() 107 | pdfDocument.finishPage(currentPage) 108 | yield() 109 | } 110 | 111 | // Create a new page in the existing 112 | val pageCount = mrenderer.pageCount 113 | val newPage = pdfDocument.startPage( 114 | PdfDocument.PageInfo.Builder( 115 | image.width, 116 | image.height, 117 | pageCount + 1 118 | ).create() 119 | ) 120 | val canvas = newPage.canvas 121 | // Draw the image on the canvas of the new page 122 | canvas.drawBitmap(image, 0f, 0f, null) 123 | 124 | // Finish the new page 125 | pdfDocument.finishPage(newPage) 126 | 127 | // Save the changes to the existing or new PDF document 128 | val outputStream = FileOutputStream(pdfFile) 129 | pdfDocument.writeTo(outputStream) 130 | if (isActive) { 131 | pdfFile.copyTo(actualFile, true) 132 | } 133 | // Close the PDF document and PDF renderer 134 | pdfDocument.close() 135 | mrenderer.close() 136 | image.recycle() 137 | } 138 | } 139 | /** 140 | * Util function to merge two pdf 141 | * imported pdf pages will get appended to old pdf 142 | * */ 143 | internal suspend fun mergePdf(oldPdfPath: String, importedPdfPath: String) { 144 | withContext(Dispatchers.IO) { 145 | val tempOldPdf = File.createTempFile("temp_old", ".pdf") 146 | val importedPdf = File(importedPdfPath) 147 | File(oldPdfPath).copyTo(tempOldPdf, true) 148 | val pdfDocument = PdfDocument() 149 | var pdfDocumentPage = 1 150 | 151 | val oldRenderer = PdfRenderer( 152 | ParcelFileDescriptor.open( 153 | tempOldPdf, 154 | ParcelFileDescriptor.MODE_READ_ONLY 155 | ) 156 | ) 157 | val newRenderer = PdfRenderer( 158 | ParcelFileDescriptor.open( 159 | importedPdf, 160 | ParcelFileDescriptor.MODE_READ_ONLY 161 | ) 162 | ) 163 | 164 | //Load old pdf pages 165 | for (i in 0 until oldRenderer.pageCount) { 166 | val originalPage = oldRenderer.openPage(i) 167 | 168 | // Create a new bitmap to draw the contents of the original page onto 169 | val bitmap = Bitmap.createBitmap( 170 | originalPage.width, 171 | originalPage.height, 172 | Bitmap.Config.ARGB_8888 173 | ) 174 | // Draw the contents of the original page onto the bitmap 175 | originalPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_PRINT) 176 | // Close the original page 177 | originalPage.close() 178 | 179 | //Create new page for bitmap 180 | val pageInfo = 181 | PdfDocument.PageInfo.Builder(bitmap.width, bitmap.height, pdfDocumentPage) 182 | .create() 183 | val currentPage = pdfDocument.startPage(pageInfo) 184 | 185 | val mCanvas = currentPage.canvas 186 | 187 | 188 | // Draw the bitmap onto the canvas of the existing page 189 | mCanvas.drawBitmap(bitmap, 0f, 0f, null) 190 | bitmap.recycle() 191 | pdfDocument.finishPage(currentPage) 192 | pdfDocumentPage += 1 193 | yield() 194 | } 195 | //Load new pdf pages 196 | for (i in 0 until newRenderer.pageCount) { 197 | val originalPage = newRenderer.openPage(i) 198 | 199 | // Create a new bitmap to draw the contents of the original page onto 200 | val bitmap = Bitmap.createBitmap( 201 | originalPage.width, 202 | originalPage.height, 203 | Bitmap.Config.ARGB_8888 204 | ) 205 | // Draw the contents of the original page onto the bitmap 206 | originalPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_PRINT) 207 | // Close the original page 208 | originalPage.close() 209 | 210 | //Create new page for bitmap 211 | val pageInfo = 212 | PdfDocument.PageInfo.Builder(bitmap.width, bitmap.height, pdfDocumentPage) 213 | .create() 214 | val currentPage = pdfDocument.startPage(pageInfo) 215 | 216 | val mCanvas = currentPage.canvas 217 | 218 | 219 | // Draw the bitmap onto the canvas of the existing page 220 | mCanvas.drawBitmap(bitmap, 0f, 0f, null) 221 | bitmap.recycle() 222 | pdfDocument.finishPage(currentPage) 223 | pdfDocumentPage += 1 224 | yield() 225 | } 226 | val outputStream = FileOutputStream(tempOldPdf) 227 | pdfDocument.writeTo(outputStream) 228 | if (isActive) { 229 | tempOldPdf.copyTo(File(oldPdfPath), true) 230 | } 231 | // Close the PDF document and PDF renderer 232 | pdfDocument.close() 233 | oldRenderer.close() 234 | newRenderer.close() 235 | } 236 | } 237 | 238 | @OptIn(ExperimentalFoundationApi::class) 239 | internal fun Modifier.pinchToZoomAndDrag() = composed { 240 | val angle by remember { mutableStateOf(0f) } 241 | var zoom by remember { mutableStateOf(1f) } 242 | var offsetX by remember { mutableStateOf(0f) } 243 | var offsetY by remember { mutableStateOf(0f) } 244 | val PI = 3.14 245 | val configuration = LocalConfiguration.current 246 | val screenWidth = configuration.screenWidthDp.dp.value * 1.2f 247 | val screenHeight = configuration.screenHeightDp.dp.value * 1.2f 248 | DisposableEffect(key1 = Unit, effect ={ 249 | onDispose { 250 | zoom = 1f 251 | offsetX = 0f 252 | offsetY = 0f 253 | } 254 | }) 255 | combinedClickable( 256 | interactionSource = remember { MutableInteractionSource() }, 257 | indication = null, 258 | onClick = {}, 259 | onDoubleClick = { 260 | zoom = if (zoom > 1f) 1f 261 | else 3f 262 | } 263 | ) 264 | .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) } 265 | .graphicsLayer( 266 | scaleX = zoom, 267 | scaleY = zoom, 268 | rotationZ = angle, 269 | ) 270 | .pointerInput(Unit) { 271 | detectTransformGestures( 272 | onGesture = { _, pan, gestureZoom, _ -> 273 | zoom = (zoom * gestureZoom).coerceIn(1F..4F) 274 | if (zoom > 1) { 275 | val x = (pan.x * zoom) 276 | val y = (pan.y * zoom) 277 | val angleRad = angle * PI / 180.0 278 | 279 | offsetX = 280 | (offsetX + (x * cos(angleRad) - y * sin(angleRad)).toFloat()).coerceIn( 281 | -(screenWidth * zoom)..(screenWidth * zoom) 282 | ) 283 | offsetY = 284 | (offsetY + (x * sin(angleRad) + y * cos(angleRad)).toFloat()).coerceIn( 285 | -(screenHeight * zoom)..(screenHeight * zoom) 286 | ) 287 | } else { 288 | offsetX = 0F 289 | offsetY = 0F 290 | } 291 | } 292 | ) 293 | } 294 | } -------------------------------------------------------------------------------- /JetPDFVue/src/main/java/com/pratikk/jetpdfvue/util/VueFileExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpdfvue.util 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.graphics.Bitmap 6 | import android.graphics.BitmapFactory 7 | import android.graphics.Matrix 8 | import android.media.ExifInterface 9 | import android.net.Uri 10 | import android.util.Base64 11 | import androidx.core.content.FileProvider 12 | import androidx.core.net.toFile 13 | import com.pratikk.jetpdfvue.state.VueFileType 14 | import java.io.ByteArrayOutputStream 15 | import java.io.File 16 | import java.io.FileInputStream 17 | import java.io.FileOutputStream 18 | import java.io.IOException 19 | import java.io.InputStream 20 | import java.text.SimpleDateFormat 21 | import java.util.Calendar 22 | import java.util.Date 23 | import java.util.Locale 24 | 25 | fun Uri.getFileType(context: Context): VueFileType { 26 | val type = context.contentResolver.getType(this) 27 | ?: scheme ?: throw Throwable("File type cannot be decoded, please check uri $this") 28 | return if (type.contains("pdf")) 29 | VueFileType.PDF 30 | else if (type.contains("text") || type.contains("txt")) 31 | VueFileType.BASE64 32 | else if(type == "file") { 33 | val file = toFile() 34 | if(file.name.contains("pdf")) 35 | VueFileType.PDF 36 | else if (file.name.contains("text") || file.name.contains("txt")) 37 | VueFileType.BASE64 38 | else 39 | VueFileType.IMAGE 40 | } else 41 | VueFileType.IMAGE 42 | } 43 | 44 | fun File.share(context: Context) { 45 | if (exists()) { 46 | //call share intent to share file 47 | val sharingIntent = Intent(Intent.ACTION_SEND) 48 | val uri = FileProvider.getUriForFile( 49 | context, 50 | context.applicationContext.packageName + ".provider", 51 | this 52 | ) 53 | if (this.name.contains("pdf")) 54 | sharingIntent.type = "application/pdf" 55 | else 56 | sharingIntent.type = "image/*" 57 | sharingIntent.putExtra(Intent.EXTRA_STREAM, uri) 58 | sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 59 | context.startActivity( 60 | Intent.createChooser( 61 | sharingIntent, 62 | "Share via" 63 | ) 64 | ) 65 | } 66 | } 67 | 68 | /** 69 | * Set the orientation to portrait 70 | * Must be called before resizing because after that the exif data would be lost 71 | * */ 72 | fun File.rotateImageIfNeeded(): File { 73 | var bitmap = BitmapFactory.decodeFile(absolutePath) 74 | 75 | //get exif of the camera while image was taken 76 | val exifInterface: ExifInterface = ExifInterface(absolutePath) 77 | val orientation = exifInterface.getAttributeInt( 78 | ExifInterface.TAG_ORIENTATION, 79 | ExifInterface.ORIENTATION_NORMAL 80 | ) 81 | val matrix = Matrix() 82 | var valChanged = false 83 | if (orientation == ExifInterface.ORIENTATION_ROTATE_90) { 84 | matrix.postRotate(90f) 85 | valChanged = true 86 | } else if (orientation == ExifInterface.ORIENTATION_ROTATE_180) { 87 | matrix.postRotate(180f) 88 | valChanged = true 89 | } else if (orientation == ExifInterface.ORIENTATION_ROTATE_270) { 90 | matrix.postRotate(270f) 91 | valChanged = true 92 | } 93 | if (valChanged) { 94 | bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) 95 | //save bitmap back to file 96 | FileOutputStream(absolutePath).use { 97 | if (absolutePath.contains("jpg") || absolutePath.contains("jpeg") || absolutePath.contains( 98 | "JPG" 99 | ) || absolutePath.contains("JPEG") 100 | ) 101 | bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) 102 | else 103 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) 104 | } 105 | } 106 | return this 107 | } 108 | 109 | 110 | /** 111 | * Function to recursively compress an image until its size is around threshold 112 | * Must be called after image has been rotated because exif data would not be retained 113 | * */ 114 | 115 | fun File.compressImageToThreshold(threshold: Int) { 116 | if (exists()) { 117 | val tempFile = File.createTempFile("tempCompress", ".$extension") 118 | copyTo(tempFile, true) 119 | var quality = 100 // Initial quality setting 120 | var currentSize = tempFile.length() 121 | 122 | while (currentSize > (threshold * 1024 * 1024)) { // 2MB in bytes 123 | quality -= 5 // Reduce quality in steps of 5 124 | if (quality < 0) { 125 | break // Don't reduce quality below 0 126 | } 127 | 128 | // Compress the image and get its new size 129 | currentSize = tempFile.compressImage(quality) 130 | } 131 | 132 | tempFile.copyTo(this,true) 133 | } 134 | } 135 | /** 136 | * Extension function to convert any input stream to a file 137 | * */ 138 | fun InputStream.toFile(extension:String):File{ 139 | val _file = File.createTempFile("temp",".${extension}") 140 | val byteArrayOutputStream = ByteArrayOutputStream() 141 | val buffer = ByteArray(1024) 142 | var bytesRead: Int 143 | while(read(buffer).also { bytesRead = it } != -1){ 144 | byteArrayOutputStream.write(buffer, 0, bytesRead) 145 | } 146 | 147 | FileOutputStream(_file).use { 148 | it.write(byteArrayOutputStream.toByteArray()) 149 | } 150 | return _file 151 | } 152 | /** 153 | * Get file from Uri 154 | * */ 155 | @Deprecated("Use toFile() to get file from uri") 156 | internal fun Uri.getFile(mContext: Context): File { 157 | val inputStream = mContext.contentResolver?.openInputStream(this) 158 | var file: File 159 | inputStream.use { input -> 160 | file = 161 | File(mContext.cacheDir, System.currentTimeMillis().toString() + ".pdf") 162 | FileOutputStream(file).use { output -> 163 | val buffer = 164 | ByteArray(4 * 1024) // or other buffer size 165 | var read: Int = -1 166 | while (input?.read(buffer).also { 167 | if (it != null) { 168 | read = it 169 | } 170 | } != -1) { 171 | output.write(buffer, 0, read) 172 | } 173 | output.flush() 174 | } 175 | } 176 | return file 177 | } 178 | 179 | /** 180 | * Copy Uri to another Uri 181 | * */ 182 | internal fun Uri.copyFile(mContext: Context, pathTo: Uri) { 183 | mContext.contentResolver?.openInputStream(this).use { inStream -> 184 | if (inStream == null) return 185 | mContext.contentResolver?.openOutputStream(pathTo).use { outStream -> 186 | if (outStream == null) return 187 | // Transfer bytes from in to out 188 | val buf = ByteArray(1024) 189 | var len: Int 190 | while (inStream.read(buf).also { len = it } > 0) { 191 | outStream.write(buf, 0, len) 192 | } 193 | } 194 | } 195 | } 196 | // Function to compress an image and return its size 197 | internal fun File.compressImage(quality: Int): Long { 198 | try { 199 | val bitmap = BitmapFactory.decodeFile(absolutePath) 200 | 201 | val outputStream = FileOutputStream(this) 202 | // Compress the bitmap with the specified quality (0-100) 203 | if (absolutePath.contains("jpg") || absolutePath.contains("jpeg") || absolutePath.contains("JPG") || absolutePath.contains( 204 | "JPEG" 205 | ) 206 | ) 207 | bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream) 208 | else 209 | bitmap.compress(Bitmap.CompressFormat.PNG, quality, outputStream) 210 | 211 | outputStream.flush() 212 | outputStream.close() 213 | 214 | // Return the size of the compressed image 215 | return length() 216 | } catch (e: IOException) { 217 | e.printStackTrace() 218 | } 219 | return 0 220 | } 221 | 222 | internal fun generateFileName(): String { 223 | return "${Calendar.getInstance().timeInMillis.getDateddMMyyyyHHmm()}_${Calendar.getInstance().timeInMillis}.pdf" 224 | } 225 | 226 | internal fun Long.getDateddMMyyyyHHmm(): String = 227 | SimpleDateFormat("dd_MM_yyyy_hh_mm", Locale.getDefault()).format(Date(this)).toString() 228 | 229 | internal fun File.toBase64File():File{ 230 | val file = File.createTempFile("temp",".txt") 231 | val inputStream = FileInputStream(this) 232 | val byteArrayOutputStream = ByteArrayOutputStream() 233 | val buffer = ByteArray(1024) 234 | var bytesRead: Int 235 | while(inputStream.read(buffer).also { bytesRead = it } != -1){ 236 | byteArrayOutputStream.write(buffer, 0, bytesRead) 237 | } 238 | 239 | val decodeStream = Base64.decode(byteArrayOutputStream.toByteArray(), Base64.DEFAULT) 240 | FileOutputStream(file, false).use { 241 | it.write(decodeStream) 242 | } 243 | return file 244 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JetPDFVue [![](https://jitpack.io/v/pratikksahu/JetPDFVue.svg)](https://jitpack.io/#pratikksahu/JetPDFVue) 2 | JetPDFVue is a library to **Create,Modify,View PDF** written in Jetpack Compose. This was created using [PDFRenderer](https://developer.android.com/reference/android/graphics/pdf/PdfRenderer) and [PDFDocument](https://developer.android.com/reference/android/graphics/pdf/PdfDocument). The library supports both Horizontal and Vertical viewing. 3 | 4 | # Examples 5 | 1. Horizontal [Example 1](app/src/main/java/com/pratikk/jetpackpdf/horizontalSamples/HorizontalSampleA.kt) [Example 2](app/src/main/java/com/pratikk/jetpackpdf/horizontalSamples/HorizontalSampleB.kt) 6 | 7 | 8 | https://github.com/pratikksahu/JetPDFVue/assets/58379829/4723eba0-825e-4df0-8e2c-93a6187d013d 9 | 10 | 11 | 12 | https://github.com/pratikksahu/JetPDFVue/assets/58379829/d153cc40-dfa4-47a3-aa25-8c3ccb00d503 13 | 14 | 15 | 16 | 17 | 2. Vertical [Example 1](app/src/main/java/com/pratikk/jetpackpdf/verticalSamples/VerticalSampleA.kt) 18 | 19 | [VerticalSampleA.webm](https://github.com/pratikksahu/JetPDFVue/assets/58379829/b5013cad-c0c0-4344-b403-5b411a86a62b) 20 | 21 | 22 | Remote sources might not work sometimes because files are hosted on google drive 23 | # Features 24 | 25 | - **Multiple Data Sources:** JetPDFVue supports various data sources out of the box, including Base64, URL, Uri, Image, and Custom. 26 | - **Interactive Viewer:** Features like pinch-to-zoom and panning make it easy to interact with PDFs. 27 | - **Page Rotation:** Rotate pages with ease. 28 | - **PDF Manipulation:** Add PDF pages or images to an existing PDF. 29 | - **Share PDF:** Share your PDF documents seamlessly. 30 | - **State Persistence:** JetPDFVue remembers the UI state across compositions, eliminating the need for a ViewModel. 31 | - **Custom Slider:** A customizable slider for navigating through pages. 32 | - **Comprehensive State Indicator:** Gain insights into the PDF's loading and importing state. 33 | - **Efficient Memory Management:** JetPDFVue incorporates cache support for efficient memory usage. 34 | - **Useful Extensions:** Simplify common tasks with extension functions, such as image rotation and compression. 35 | 36 | - `File.rotateImageIfNeeded()`: Rotate images to portrait orientation during import. 37 | - `File.compressImageToThreshold(threshold: Int)`: Compress images during import. 38 | - `InputStream.toFile(extension: String)`: Convert any input stream to a file. 39 | 40 | # Get Started 41 | ## Integrate 42 | **Step 1.** Add `INTERNET` permissions on your AndroidManifest.xml 43 | 44 | ```xml 45 | 46 | ``` 47 | **Step 2.** Add the JitPack maven repository 48 | 49 | ```gradle 50 | maven { url "https://jitpack.io" } 51 | ``` 52 | **Step 3.** Add the dependency 53 | 54 | ```gradle 55 | dependencies { 56 | implementation("com.github.pratikksahu:JetPDFVue:1.0.7") 57 | } 58 | ``` 59 | ## How to use 60 | **Step 4.** You can use the library by creating the state in a Composable 61 | #### This is for horizontal viewing 62 | 63 | ```kotlin 64 | val horizontalVueReaderState = rememberHorizontalVueReaderState( 65 | resource = VueResourceType.Local( 66 | uri = context.resources.openRawResource( 67 | R.raw.lorem_ipsum 68 | ).toFile("pdf").toUri(), 69 | fileType = VueFileType.PDF 70 | ), 71 | cache = 3 // By default 0 72 | ) 73 | 74 | // .toFile is an util extension function to convert any input stream to a file 75 | ``` 76 | #### This is for vertical viewing 77 | ```kotlin 78 | val verticalVueReaderState = rememberVerticalVueReaderState( 79 | resource = VueResourceType.Local( 80 | uri = context.resources.openRawResource( 81 | R.raw.lorem_ipsum 82 | ).toFile("pdf").toUri(), 83 | fileType = VueFileType.PDF 84 | ), 85 | cache = 3 // By default 0 86 | ) 87 | 88 | // .toFile is an util extension function to convert any input stream to a file 89 | ``` 90 | **Step 5.** Invoke load() method to initalize source 91 | ```kotlin 92 | LaunchedEffect(Unit) { 93 | horizontalVueReaderState.load( // or verticalVueReaderState.load() 94 | context = context, 95 | coroutineScope = scope, 96 | containerSize = containerSize, // Used to create a canvas for bitmap 97 | isPortrait = true, //Use LocalConfiguration to determine orientation 98 | customResource = null // Requires when using Custom as Resource type 99 | ) 100 | } 101 | ``` 102 | **Step 6.** Observe the reader state 103 | ```kotlin 104 | val vueLoadState = horizontalVueReaderState.vueLoadState 105 | when(vueLoadState){ 106 | is VueLoadState.DocumentError -> { 107 | /** 108 | * Handle Error by using 109 | * vueLoadState.getErrorMessage 110 | * */ 111 | } 112 | VueLoadState.DocumentImporting -> { 113 | /** 114 | * Indicates when image/pdf is being imported 115 | * This is also the state when the image is done importing but is being processed 116 | * */ 117 | } 118 | VueLoadState.DocumentLoaded -> { 119 | /** 120 | * This is the state where either 121 | * HorizontalPdfViewer(horizontalVueReaderState = horizontalVueReaderState) 122 | * or 123 | * VerticalPdfViewer(verticalVueReaderState = verticalVueReaderState) 124 | * Is used to display pdf 125 | * */ 126 | } 127 | VueLoadState.DocumentLoading -> { 128 | /** 129 | * Indicates when image/pdf is loaded initially 130 | * This is also the state when resource type is custom 131 | * Use horizontalVueReaderState.loadPercent to get progress (Does not work with Custom resource) 132 | * */ 133 | } 134 | VueLoadState.NoDocument -> { 135 | /** 136 | * This is the state where you want to create a new document 137 | * Here, show UI for ex, button to launch the import intent 138 | * */ 139 | } 140 | } 141 | ``` 142 | **Step 7.** HorizontalVueReader and VerticalVueReader Should be used only when in `VueLoadState.DocumentLoaded` State 143 | ```kotlin 144 | is VueLoadState.DocumentLoaded -> { 145 | HorizontalVueReader( 146 | modifier = Modifier, // Modifier for pager 147 | contentModifier = Modifier, // Modifier for Individual page 148 | horizontalVueReaderState = horizontalVueReaderState 149 | ) 150 | } 151 | ``` 152 | # [Resource Type](JetPDFVue/src/main/java/com/pratikk/jetpdfvue/state/VueResourceType.kt) 153 | ### Remote 154 | 1. Base64 155 | ```kotlin 156 | rememberHorizontalVueReaderState( 157 | resource = VueResourceType.Remote( 158 | "https://drive.google.com/uc?export=download&id=1-mmdJ2K2x3MDgTqmFd8sMpW3zIFyNYY-", 159 | fileType = VueFileType.BASE64 160 | ) 161 | ) 162 | ``` 163 | 2. PDF 164 | ```kotlin 165 | rememberHorizontalVueReaderState( 166 | resource = VueResourceType.Remote( 167 | "https://drive.google.com/uc?export=download&id=1DSA7cmFzqCtTsHhlB0xdYJ6UweuC8IOz", 168 | fileType = VueFileType.PDF 169 | ) 170 | ) 171 | ``` 172 | 3. Image 173 | ```kotlin 174 | rememberHorizontalVueReaderState( 175 | resource = VueResourceType.Remote( 176 | "InsertyYourImageLink.com", 177 | fileType = VueFileType.IMAGE 178 | ) 179 | ) 180 | ``` 181 | ### Local 182 | 1. Base64 183 | ```kotlin 184 | rememberHorizontalVueReaderState( 185 | resource = VueResourceType.Local( 186 | uri = , 187 | fileType = VueFileType.BASE64 188 | ) 189 | ) 190 | ``` 191 | 2. PDF 192 | ```kotlin 193 | rememberHorizontalVueReaderState( 194 | resource = VueResourceType.Local( 195 | uri = , 196 | fileType = VueFileType.PDF 197 | ) 198 | ) 199 | ``` 200 | 3. Image 201 | ```kotlin 202 | rememberHorizontalVueReaderState( 203 | resource = VueResourceType.Local( 204 | uri = , 205 | fileType = VueFileType.IMAGE 206 | ) 207 | ) 208 | ``` 209 | ### Asset 210 | 1. Base64 211 | ```kotlin 212 | rememberHorizontalVueReaderState( 213 | resource = VueResourceType.Asset( 214 | assetId = , 215 | fileType = VueFileType.BASE64 216 | ) 217 | ) 218 | ``` 219 | 2. PDF 220 | ```kotlin 221 | rememberHorizontalVueReaderState( 222 | resource = VueResourceType.Asset( 223 | assetId = , 224 | fileType = VueFileType.PDF 225 | ) 226 | ) 227 | ``` 228 | 3. Image 229 | ```kotlin 230 | rememberHorizontalVueReaderState( 231 | resource = VueResourceType.Asset( 232 | assetId = , 233 | fileType = VueFileType.IMAGE 234 | ) 235 | ) 236 | ``` 237 | 238 | ### Blank Document 239 | This state is added in case of showing the preview on the same compose without navigating. 240 | [VueReader]() file picker can be used to create a pdf. 241 | This resource type provides an additional state [NoDocument](JetPDFVue/src/main/java/com/pratikk/jetpdfvue/state/VueLoadState.kt) which can be used to show UI for importing any document/image. 242 | 243 | ```kotlin 244 | rememberHorizontalVueReaderState(resource = VueResourceType.BlankDocument()) 245 | ``` 246 | ### Custom 247 | Any network request or transformation can be done in this scope 248 | ```kotlin 249 | rememberHorizontalVueReaderState(resource = VueResourceType.Custom) 250 | ``` 251 | ```kotlin 252 | LaunchedEffect(key1 = Unit, block = { 253 | horizontalVueReaderState.load( 254 | context = context, 255 | coroutineScope = this, 256 | containerSize = containerSize, 257 | isPortrait = true, 258 | customResource = { // This lambda will be invoked when using Custom resource type 259 | networkCall() // Should return a file 260 | }) 261 | }) 262 | ``` 263 | # Import PDF and Images 264 | ## This launcher should be used when resource type is of [VueResourceType](JetPDFVue/src/main/java/com/pratikk/jetpdfvue/state/VueResourceType.kt) 265 | ### 1. Create launcher 266 | ```kotlin 267 | val launcher = horizontalVueReaderState.getImportLauncher(interceptResult = {file -> 268 | // This lambda will be invoked only when imported type is an image 269 | // Use this to reduce file size,rotate or transform as per your need 270 | file 271 | .rotateImageIfNeeded() 272 | .compressImageToThreshold(2) 273 | }) 274 | ``` 275 | ### 2. Launch Import Intent 276 | ```kotlin 277 | horizontalVueReaderState.launchImportIntent( 278 | context = context, 279 | launcher = launcher 280 | ) 281 | ``` 282 | ## General file picker to import image from gallery/camera, pdf from device storage 283 | ### 1. Create launcher 284 | ```kotlin 285 | val vueFilePicker = rememberSaveable(saver = VueFilePicker.Saver) { 286 | VueFilePicker() 287 | } 288 | val launcher = vueFilePicker.getLauncher( 289 | interceptResult = { 290 | //Perform file operation on imported file 291 | }, 292 | onResult = { 293 | //Get the final file 294 | } 295 | ) 296 | ``` 297 | ### 2. Launch Import Intent 298 | ```kotlin 299 | vueFilePicker.launchIntent( 300 | context = context, 301 | vueImportSources = listOf( 302 | VueImportSources.CAMERA, 303 | VueImportSources.GALLERY, 304 | VueImportSources.PDF, 305 | VueImportSources.BASE64 306 | ), 307 | launcher = launcher 308 | ) 309 | ``` 310 | # Share PDF 311 | ```kotlin 312 | horizontalVueReaderState.sharePDF(context) 313 | ``` 314 | # Feature Implementations 315 | 316 | ### 1. Page rotation 317 | The rotation is being done on bitmap level but you don't have to worry about that Just use `horizontalVueReaderState.rotate(angle)` 318 | 319 | ### 2. Custom Slider 320 | Zoom gesture and swipe gesture does not seem to be working together but there is a custom slider to move between pages. 321 | 322 | ```kotlin 323 | VueHorizontalSlider( 324 | modifier = Modifier 325 | .padding(horizontal = 10.dp, vertical = 10.dp) 326 | .fillMaxWidth() 327 | .height(40.dp), 328 | horizontalVueReaderState = horizontalVueReaderState 329 | ) 330 | ``` 331 | Helper function is also available if you choose not to use the slider 332 | ```kotlin 333 | scope.launch {horizontalVueReaderState.prevPage()} 334 | and 335 | scope.launch {horizontalVueReaderState.nextPage()} 336 | ``` 337 | 338 | 339 | ### License 340 | ```xml 341 | Copyright [2023] [Pratik Sahu] 342 | 343 | Licensed under the Apache License, Version 2.0 (the "License"); 344 | you may not use this file except in compliance with the License. 345 | You may obtain a copy of the License at 346 | 347 | http://www.apache.org/licenses/LICENSE-2.0 348 | 349 | Unless required by applicable law or agreed to in writing, software 350 | distributed under the License is distributed on an "AS IS" BASIS, 351 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 352 | See the License for the specific language governing permissions and 353 | limitations under the License. 354 | ``` 355 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | } 5 | 6 | android { 7 | namespace = "com.pratikk.jetpackpdf" 8 | compileSdk = 35 9 | 10 | defaultConfig { 11 | applicationId = "com.pratikk.jetpackpdf" 12 | minSdk = 24 13 | targetSdk = 35 14 | versionCode = 1 15 | versionName = "1.0" 16 | 17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 18 | vectorDrawables { 19 | useSupportLibrary = true 20 | } 21 | } 22 | 23 | buildTypes { 24 | release { 25 | isMinifyEnabled = false 26 | proguardFiles( 27 | getDefaultProguardFile("proguard-android-optimize.txt"), 28 | "proguard-rules.pro" 29 | ) 30 | } 31 | } 32 | compileOptions { 33 | sourceCompatibility = JavaVersion.VERSION_17 34 | targetCompatibility = JavaVersion.VERSION_17 35 | } 36 | kotlinOptions { 37 | jvmTarget = "17" 38 | } 39 | buildFeatures { 40 | compose = true 41 | } 42 | composeOptions { 43 | kotlinCompilerExtensionVersion = "1.5.1" 44 | } 45 | packaging { 46 | resources { 47 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 48 | } 49 | } 50 | 51 | } 52 | 53 | dependencies { 54 | implementation(project(":JetPDFVue")) 55 | implementation("androidx.core:core-ktx:1.15.0") 56 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") 57 | implementation("androidx.activity:activity-compose:1.9.3") 58 | implementation(platform("androidx.compose:compose-bom:2024.10.01")) 59 | implementation("androidx.compose.ui:ui") 60 | implementation("androidx.compose.ui:ui-graphics") 61 | implementation("androidx.compose.ui:ui-tooling-preview") 62 | implementation("androidx.compose.material3:material3") 63 | 64 | testImplementation("junit:junit:4.13.2") 65 | androidTestImplementation("androidx.test.ext:junit:1.1.5") 66 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") 67 | androidTestImplementation(platform("androidx.compose:compose-bom:2023.10.01")) 68 | androidTestImplementation("androidx.compose.ui:ui-test-junit4") 69 | debugImplementation("androidx.compose.ui:ui-tooling") 70 | debugImplementation("androidx.compose.ui:ui-test-manifest") 71 | implementation("androidx.compose.material:material-icons-extended") 72 | 73 | //Navigation 74 | implementation("androidx.navigation:navigation-compose:2.7.3") 75 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/pratikk/jetpackpdf/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpackpdf 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.pratikk.jetpackpdf", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 36 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/assets/demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratikksahu/JetPDFVue/c763f7b84fc44ac336b4af697911409a75aea009/app/src/main/assets/demo.jpg -------------------------------------------------------------------------------- /app/src/main/assets/lorem_ipsum.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratikksahu/JetPDFVue/c763f7b84fc44ac336b4af697911409a75aea009/app/src/main/assets/lorem_ipsum.pdf -------------------------------------------------------------------------------- /app/src/main/java/com/pratikk/jetpackpdf/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpackpdf 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.material.icons.Icons 13 | import androidx.compose.material.icons.outlined.Info 14 | import androidx.compose.material3.Divider 15 | import androidx.compose.material3.ExperimentalMaterial3Api 16 | import androidx.compose.material3.Icon 17 | import androidx.compose.material3.IconButton 18 | import androidx.compose.material3.MaterialTheme 19 | import androidx.compose.material3.Scaffold 20 | import androidx.compose.material3.Surface 21 | import androidx.compose.material3.Switch 22 | import androidx.compose.material3.Text 23 | import androidx.compose.material3.TopAppBar 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.runtime.getValue 26 | import androidx.compose.runtime.mutableStateOf 27 | import androidx.compose.runtime.saveable.rememberSaveable 28 | import androidx.compose.runtime.setValue 29 | import androidx.compose.ui.Alignment 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.platform.LocalContext 32 | import androidx.compose.ui.text.font.FontWeight 33 | import androidx.compose.ui.text.style.TextAlign 34 | import androidx.compose.ui.unit.dp 35 | import androidx.core.net.toUri 36 | import androidx.navigation.NamedNavArgument 37 | import androidx.navigation.NavHost 38 | import androidx.navigation.NavType 39 | import androidx.navigation.compose.NavHost 40 | import androidx.navigation.compose.composable 41 | import androidx.navigation.compose.rememberNavController 42 | import androidx.navigation.navArgument 43 | import com.pratikk.jetpackpdf.horizontalSamples.HorizontalPdfViewer 44 | import com.pratikk.jetpackpdf.horizontalSamples.HorizontalPdfViewerLocal 45 | import com.pratikk.jetpackpdf.ui.theme.JetpackPDFTheme 46 | import com.pratikk.jetpackpdf.verticalSamples.VerticalPdfViewer 47 | import com.pratikk.jetpackpdf.verticalSamples.VerticalPdfViewerLocal 48 | import com.pratikk.jetpdfvue.state.VueFilePicker 49 | import com.pratikk.jetpdfvue.state.VueFileType 50 | import com.pratikk.jetpdfvue.state.VueResourceType 51 | import com.pratikk.jetpdfvue.state.rememberHorizontalVueReaderState 52 | import com.pratikk.jetpdfvue.state.rememberVerticalVueReaderState 53 | import com.pratikk.jetpdfvue.util.toFile 54 | 55 | 56 | class MainActivity : ComponentActivity() { 57 | @OptIn(ExperimentalMaterial3Api::class) 58 | override fun onCreate(savedInstanceState: Bundle?) { 59 | super.onCreate(savedInstanceState) 60 | setContent { 61 | JetpackPDFTheme { 62 | val navController = rememberNavController() 63 | // A surface container using the 'background' color from the theme 64 | Scaffold(topBar = { 65 | Column { 66 | TopAppBar(title = { Text(text = "JetPDFVue Sample") }) 67 | Divider() 68 | } 69 | } 70 | ) { 71 | Surface( 72 | modifier = Modifier 73 | .fillMaxSize() 74 | .padding(it), 75 | color = MaterialTheme.colorScheme.background 76 | ) { 77 | NavHost( 78 | navController = navController, startDestination = "Home"){ 79 | composable(route = "Home"){ 80 | HomeOptions( 81 | modifier = Modifier 82 | .fillMaxSize(), 83 | onSelection = {isVertical, sampleResourceType -> 84 | if(!isVertical){ 85 | navController.navigate("Horizontal/$sampleResourceType") 86 | }else{ 87 | navController.navigate("Vertical/$sampleResourceType") 88 | } 89 | } 90 | ) 91 | } 92 | composable(route = "Horizontal/{type}", 93 | arguments = listOf( 94 | navArgument("type"){ 95 | type = NavType.IntType 96 | } 97 | ) 98 | ){ 99 | val context = LocalContext.current 100 | val type = it.arguments?.getInt("type")!! 101 | when(type){ 102 | 1 -> { 103 | //Local 104 | HorizontalPdfViewerLocal() 105 | } 106 | 2 -> { 107 | //Asset as local 108 | val localImage = rememberHorizontalVueReaderState( 109 | resource = VueResourceType.Local( 110 | uri = context.resources.openRawResource( 111 | R.raw.demo 112 | ).toFile("jpg").toUri(), 113 | fileType = VueFileType.IMAGE 114 | ), 115 | ) 116 | /** 117 | Other file types 118 | val localPdf = rememberHorizontalVueReaderState( 119 | resource = VueResourceType.Local( 120 | uri = context.resources.openRawResource( 121 | R.raw.lorem_ipsum 122 | ).toFile(".pdf").toUri(), 123 | fileType = VueFileType.PDF 124 | ) 125 | ) 126 | val localBase64 = rememberHorizontalVueReaderState( 127 | resource = VueResourceType.Local( 128 | uri = context.resources.openRawResource( 129 | R.raw.lorem_ipsum_base64 130 | ).toFile(".txt").toUri(), 131 | fileType = VueFileType.BASE64 132 | ) 133 | ) 134 | */ 135 | HorizontalPdfViewer(horizontalVueReaderState = localImage) 136 | } 137 | 3 -> { 138 | //Remote 139 | val remoteImageLink = listOf( 140 | "https://images.pexels.com/photos/943907/pexels-photo-943907.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2", 141 | "https://images.freeimages.com/images/large-previews/7f3/path-1441068.jpg" 142 | ) 143 | 144 | val remotePdf = 145 | rememberHorizontalVueReaderState( 146 | resource = VueResourceType.Remote( 147 | "https://drive.google.com/uc?export=download&id=1DSA7cmFzqCtTsHhlB0xdYJ6UweuC8IOz", 148 | fileType = VueFileType.PDF 149 | ) 150 | ) 151 | /** 152 | * Other File Types 153 | val remoteImage = 154 | rememberHorizontalVueReaderState( 155 | resource = VueResourceType.Remote( 156 | remoteImageLink[0], 157 | fileType = VueFileType.IMAGE 158 | ) 159 | ) 160 | 161 | val remoteBase64 = 162 | rememberHorizontalVueReaderState( 163 | resource = VueResourceType.Remote( 164 | "https://drive.google.com/uc?export=download&id=1-mmdJ2K2x3MDgTqmFd8sMpW3zIFyNYY-", 165 | fileType = VueFileType.BASE64 166 | ) 167 | ) 168 | */ 169 | HorizontalPdfViewer(horizontalVueReaderState = remotePdf) 170 | } 171 | 4 -> { 172 | //Asset 173 | val assetPdf = rememberHorizontalVueReaderState( 174 | resource = VueResourceType.Asset(assetId = R.raw.lorem_ipsum, fileType = VueFileType.PDF) 175 | ) 176 | /** 177 | * Other File Types 178 | val assetImage = rememberHorizontalVueReaderState( 179 | resource = VueResourceType.Asset(assetId = R.raw.demo, fileType = VueFileType.IMAGE) 180 | ) 181 | 182 | val assetBase64 = rememberHorizontalVueReaderState( 183 | resource = VueResourceType.Asset( 184 | assetId = R.raw.lorem_ipsum_base64, 185 | fileType = VueFileType.BASE64 186 | ) 187 | ) 188 | */ 189 | HorizontalPdfViewer(horizontalVueReaderState = assetPdf) 190 | } 191 | 5 -> { 192 | val blankState = rememberHorizontalVueReaderState(resource = VueResourceType.BlankDocument()) 193 | HorizontalPdfViewer(horizontalVueReaderState = blankState) 194 | } 195 | } 196 | } 197 | composable(route = "Vertical/{type}", 198 | arguments = listOf( 199 | navArgument("type"){ 200 | type = NavType.IntType 201 | } 202 | )){ 203 | val context = LocalContext.current 204 | val type = it.arguments?.getInt("type")!! 205 | when(type){ 206 | 1 -> { 207 | //Local 208 | VerticalPdfViewerLocal() 209 | } 210 | 2 -> { 211 | //Asset as local 212 | val localImage = rememberVerticalVueReaderState( 213 | resource = VueResourceType.Local( 214 | uri = context.resources.openRawResource( 215 | R.raw.demo 216 | ).toFile("jpg").toUri(), 217 | fileType = VueFileType.IMAGE 218 | ), 219 | ) 220 | /** 221 | Other file types 222 | val localPdf = rememberVerticalVueReaderState( 223 | resource = VueResourceType.Local( 224 | uri = context.resources.openRawResource( 225 | R.raw.lorem_ipsum 226 | ).toFile(".pdf").toUri(), 227 | fileType = VueFileType.PDF 228 | ) 229 | ) 230 | val localBase64 = rememberVerticalVueReaderState( 231 | resource = VueResourceType.Local( 232 | uri = context.resources.openRawResource( 233 | R.raw.lorem_ipsum_base64 234 | ).toFile(".txt").toUri(), 235 | fileType = VueFileType.BASE64 236 | ) 237 | ) 238 | */ 239 | VerticalPdfViewer(verticalVueReaderState = localImage) 240 | } 241 | 3 -> { 242 | //Remote 243 | val remoteImageLink = listOf( 244 | "https://images.pexels.com/photos/943907/pexels-photo-943907.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2", 245 | "https://images.freeimages.com/images/large-previews/7f3/path-1441068.jpg" 246 | ) 247 | 248 | val remotePdf = 249 | rememberVerticalVueReaderState( 250 | resource = VueResourceType.Remote( 251 | "https://drive.google.com/uc?export=download&id=1DSA7cmFzqCtTsHhlB0xdYJ6UweuC8IOz", 252 | fileType = VueFileType.PDF 253 | ) 254 | ) 255 | /** 256 | * Other File Types 257 | val remoteImage = 258 | rememberVerticalVueReaderState( 259 | resource = VueResourceType.Remote( 260 | remoteImageLink[0], 261 | fileType = VueFileType.IMAGE 262 | ) 263 | ) 264 | 265 | val remoteBase64 = 266 | rememberVerticalVueReaderState( 267 | resource = VueResourceType.Remote( 268 | "https://drive.google.com/uc?export=download&id=1-mmdJ2K2x3MDgTqmFd8sMpW3zIFyNYY-", 269 | fileType = VueFileType.BASE64 270 | ) 271 | ) 272 | */ 273 | VerticalPdfViewer(verticalVueReaderState = remotePdf) 274 | } 275 | 4 -> { 276 | //Asset 277 | val assetPdf = rememberVerticalVueReaderState( 278 | resource = VueResourceType.Asset(assetId = R.raw.lorem_ipsum, fileType = VueFileType.PDF) 279 | ) 280 | /** 281 | * Other File Types 282 | val assetImage = rememberVerticalVueReaderState( 283 | resource = VueResourceType.Asset(assetId = R.raw.demo, fileType = VueFileType.IMAGE) 284 | ) 285 | 286 | val assetBase64 = rememberVerticalVueReaderState( 287 | resource = VueResourceType.Asset( 288 | assetId = R.raw.lorem_ipsum_base64, 289 | fileType = VueFileType.BASE64 290 | ) 291 | ) 292 | */ 293 | VerticalPdfViewer(verticalVueReaderState = assetPdf) 294 | } 295 | 5 -> { 296 | val blankState = rememberVerticalVueReaderState(resource = VueResourceType.BlankDocument()) 297 | VerticalPdfViewer(verticalVueReaderState = blankState) 298 | } 299 | } 300 | } 301 | } 302 | } 303 | } 304 | 305 | } 306 | } 307 | } 308 | } 309 | 310 | @Composable 311 | fun HomeOptions(modifier: Modifier = Modifier, 312 | onSelection:(isVertical:Boolean, sampleResourceType:Int) -> Unit) { 313 | var isVertical by rememberSaveable { 314 | mutableStateOf(false) 315 | } 316 | Column(modifier = modifier) { 317 | Text( 318 | modifier = Modifier.padding(vertical = 12.dp, horizontal = 8.dp), 319 | text = "Sample Source", 320 | fontWeight = FontWeight.Bold, 321 | style = MaterialTheme.typography.titleLarge) 322 | Row(modifier = Modifier 323 | .clickable { 324 | onSelection(isVertical, 1) 325 | } 326 | .fillMaxWidth() 327 | .padding(horizontal = 16.dp, vertical = 12.dp), 328 | verticalAlignment = Alignment.CenterVertically) { 329 | Text( 330 | modifier = Modifier.weight(1f), 331 | text = "Local",style = MaterialTheme.typography.bodyLarge) 332 | } 333 | Divider(Modifier.padding(start = 14.dp)) 334 | Row(modifier = Modifier 335 | .clickable { 336 | onSelection(isVertical, 2) 337 | } 338 | .fillMaxWidth() 339 | .padding(horizontal = 16.dp, vertical = 12.dp), 340 | verticalAlignment = Alignment.CenterVertically) { 341 | Text( 342 | modifier = Modifier.weight(1f), 343 | text = "Asset as local ",style = MaterialTheme.typography.bodyLarge) 344 | } 345 | Divider(Modifier.padding(start = 14.dp)) 346 | Row(modifier = Modifier 347 | .clickable { 348 | onSelection(isVertical, 3) 349 | } 350 | .fillMaxWidth() 351 | .padding(horizontal = 16.dp, vertical = 12.dp), 352 | verticalAlignment = Alignment.CenterVertically) { 353 | Text( 354 | modifier = Modifier.weight(1f), 355 | text = "Remote",style = MaterialTheme.typography.bodyLarge) 356 | } 357 | Divider(Modifier.padding(start = 14.dp)) 358 | Row(modifier = Modifier 359 | .clickable { 360 | onSelection(isVertical, 4) 361 | } 362 | .fillMaxWidth() 363 | .padding(horizontal = 16.dp, vertical = 12.dp), 364 | verticalAlignment = Alignment.CenterVertically) { 365 | Text( 366 | modifier = Modifier.weight(1f), 367 | text = "Asset",style = MaterialTheme.typography.bodyLarge) 368 | } 369 | Divider(Modifier.padding(start = 14.dp)) 370 | Row(modifier = Modifier 371 | .clickable { 372 | onSelection(isVertical, 5) 373 | } 374 | .fillMaxWidth() 375 | .padding(horizontal = 16.dp, vertical = 12.dp), 376 | verticalAlignment = Alignment.CenterVertically) { 377 | Text( 378 | modifier = Modifier.weight(1f), 379 | text = "Blank Document",style = MaterialTheme.typography.bodyLarge) 380 | } 381 | Divider(Modifier.padding(start = 14.dp)) 382 | Row(modifier = Modifier 383 | .fillMaxWidth() 384 | .padding(horizontal = 16.dp, vertical = 16.dp), 385 | verticalAlignment = Alignment.CenterVertically) { 386 | Text( 387 | modifier = Modifier.weight(1f), 388 | text = "Horizontal View", 389 | style = MaterialTheme.typography.bodyLarge, 390 | textAlign = TextAlign.Center) 391 | Switch(checked = isVertical, onCheckedChange = { 392 | isVertical = it 393 | }) 394 | Text( 395 | modifier = Modifier.weight(1f), 396 | text = "Vertical View", 397 | style = MaterialTheme.typography.bodyLarge, 398 | textAlign = TextAlign.Center) 399 | } 400 | } 401 | } 402 | 403 | -------------------------------------------------------------------------------- /app/src/main/java/com/pratikk/jetpackpdf/horizontalSamples/HorizontalPdfViewer.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpackpdf.horizontalSamples 2 | 3 | import android.content.res.Configuration 4 | import android.net.Uri 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.BoxWithConstraints 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.material3.Button 10 | import androidx.compose.material3.CircularProgressIndicator 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.LaunchedEffect 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.rememberCoroutineScope 18 | import androidx.compose.runtime.saveable.rememberSaveable 19 | import androidx.compose.runtime.setValue 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.platform.LocalConfiguration 23 | import androidx.compose.ui.platform.LocalContext 24 | import androidx.compose.ui.unit.IntSize 25 | import androidx.core.net.toUri 26 | import com.pratikk.jetpdfvue.state.HorizontalVueReaderState 27 | import com.pratikk.jetpdfvue.state.VueFilePicker 28 | import com.pratikk.jetpdfvue.state.VueFilePickerState 29 | import com.pratikk.jetpdfvue.state.VueImportSources 30 | import com.pratikk.jetpdfvue.state.VueFileType 31 | import com.pratikk.jetpdfvue.state.VueLoadState 32 | import com.pratikk.jetpdfvue.state.VueResourceType 33 | import com.pratikk.jetpdfvue.state.rememberHorizontalVueReaderState 34 | import com.pratikk.jetpdfvue.util.compressImageToThreshold 35 | import com.pratikk.jetpdfvue.util.getFileType 36 | import kotlinx.coroutines.launch 37 | 38 | @Composable 39 | fun HorizontalPdfViewerLocal(){ 40 | val context = LocalContext.current 41 | val vueFilePicker = rememberSaveable(saver = VueFilePicker.Saver) { 42 | VueFilePicker() 43 | } 44 | var uri:Uri by rememberSaveable(stateSaver = VueFilePicker.UriSaver) { 45 | mutableStateOf(Uri.EMPTY) 46 | } 47 | val launcher = vueFilePicker.getLauncher( 48 | onResult = { 49 | uri = it.toUri() 50 | }) 51 | if(uri == Uri.EMPTY){ 52 | Column(modifier = Modifier.fillMaxSize(), 53 | horizontalAlignment = Alignment.CenterHorizontally, 54 | verticalArrangement = Arrangement.Center) { 55 | if(vueFilePicker.isImporting) 56 | CircularProgressIndicator() 57 | Button(onClick = { vueFilePicker.launchIntent(context, listOf(VueImportSources.CAMERA,VueImportSources.GALLERY,VueImportSources.PDF,VueImportSources.BASE64),launcher)}) { 58 | Text(text = "Import Document") 59 | } 60 | } 61 | }else { 62 | val localImage = rememberHorizontalVueReaderState( 63 | resource = VueResourceType.Local( 64 | uri = uri, 65 | fileType = uri.getFileType(context) 66 | ) 67 | ) 68 | HorizontalPdfViewer(horizontalVueReaderState = localImage) 69 | } 70 | } 71 | 72 | @Composable 73 | fun HorizontalPdfViewer(horizontalVueReaderState: HorizontalVueReaderState) { 74 | val context = LocalContext.current 75 | val scope = rememberCoroutineScope() 76 | 77 | val launcher = horizontalVueReaderState.getImportLauncher(interceptResult = { 78 | it.compressImageToThreshold(2) 79 | }) 80 | 81 | BoxWithConstraints( 82 | modifier = Modifier, 83 | contentAlignment = Alignment.Center 84 | ) { 85 | val configuration = LocalConfiguration.current 86 | val containerSize = remember { 87 | IntSize(constraints.maxWidth, constraints.maxHeight) 88 | } 89 | 90 | LaunchedEffect(Unit) { 91 | horizontalVueReaderState.load( 92 | context = context, 93 | coroutineScope = scope, 94 | containerSize = containerSize, 95 | isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT, 96 | customResource = null 97 | ) 98 | } 99 | val vueLoadState = horizontalVueReaderState.vueLoadState 100 | when(vueLoadState){ 101 | is VueLoadState.DocumentError -> { 102 | /** 103 | * Handle Error by using 104 | * vueLoadState.getErrorMessage 105 | * */ 106 | } 107 | VueLoadState.DocumentImporting -> { 108 | /** 109 | * Indicates when image/pdf is being imported 110 | * This is also the state when the image is done importing but is being processed 111 | * */ 112 | } 113 | VueLoadState.DocumentLoaded -> { 114 | /** 115 | * This is the state where either 116 | * HorizontalPdfViewer(horizontalVueReaderState = horizontalVueReaderState) 117 | * or 118 | * VerticalPdfViewer(verticalVueReaderState = verticalVueReaderState) 119 | * Is used to display pdf 120 | * */ 121 | } 122 | VueLoadState.DocumentLoading -> { 123 | /** 124 | * Indicates when image/pdf is loaded initially 125 | * This is also the state when resource type is custom 126 | * */ 127 | } 128 | VueLoadState.NoDocument -> { 129 | /** 130 | * This is the state where you want to create a new document 131 | * Here, show UI for ex, button to launch the import intent 132 | * */ 133 | } 134 | } 135 | when (vueLoadState) { 136 | is VueLoadState.NoDocument -> { 137 | Button(onClick = { 138 | horizontalVueReaderState.launchImportIntent( 139 | context = context, 140 | launcher = launcher 141 | ) 142 | }) { 143 | Text(text = "Import Document") 144 | } 145 | } 146 | 147 | is VueLoadState.DocumentError -> { 148 | Column { 149 | Text(text = "Error: ${horizontalVueReaderState.vueLoadState.getErrorMessage}") 150 | Button(onClick = { 151 | scope.launch { 152 | horizontalVueReaderState.load( 153 | context = context, 154 | coroutineScope = scope, 155 | containerSize = containerSize, 156 | isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT, 157 | customResource = null 158 | ) 159 | } 160 | }) { 161 | Text(text = "Retry") 162 | } 163 | } 164 | } 165 | 166 | is VueLoadState.DocumentImporting -> { 167 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 168 | CircularProgressIndicator() 169 | Text(text = "Importing...") 170 | } 171 | } 172 | 173 | is VueLoadState.DocumentLoaded -> { 174 | HorizontalSampleB(horizontalVueReaderState = horizontalVueReaderState) { 175 | horizontalVueReaderState.launchImportIntent( 176 | context = context, 177 | launcher = launcher 178 | ) 179 | } 180 | } 181 | 182 | is VueLoadState.DocumentLoading -> { 183 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 184 | CircularProgressIndicator() 185 | Text(text = "Loading ${horizontalVueReaderState.loadPercent}") 186 | } 187 | } 188 | } 189 | } 190 | } -------------------------------------------------------------------------------- /app/src/main/java/com/pratikk/jetpackpdf/horizontalSamples/HorizontalSampleA.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpackpdf.horizontalSamples 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.border 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.BoxWithConstraints 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.layout.width 14 | import androidx.compose.material.icons.Icons 15 | import androidx.compose.material.icons.filled.Add 16 | import androidx.compose.material.icons.filled.KeyboardArrowLeft 17 | import androidx.compose.material.icons.filled.KeyboardArrowRight 18 | import androidx.compose.material.icons.filled.RotateLeft 19 | import androidx.compose.material.icons.filled.RotateRight 20 | import androidx.compose.material.icons.filled.Share 21 | import androidx.compose.material3.Icon 22 | import androidx.compose.material3.IconButton 23 | import androidx.compose.material3.MaterialTheme 24 | import androidx.compose.material3.Text 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.runtime.derivedStateOf 27 | import androidx.compose.runtime.getValue 28 | import androidx.compose.runtime.remember 29 | import androidx.compose.runtime.rememberCoroutineScope 30 | import androidx.compose.ui.Alignment 31 | import androidx.compose.ui.Modifier 32 | import androidx.compose.ui.draw.clip 33 | import androidx.compose.ui.graphics.Color 34 | import androidx.compose.ui.platform.LocalContext 35 | import androidx.compose.ui.unit.dp 36 | import com.pratikk.jetpdfvue.HorizontalVueReader 37 | import com.pratikk.jetpdfvue.state.HorizontalVueReaderState 38 | import kotlinx.coroutines.launch 39 | 40 | @Composable 41 | fun HorizontalSampleA( 42 | modifier: Modifier = Modifier, 43 | horizontalVueReaderState: HorizontalVueReaderState, 44 | import:() -> Unit 45 | ) { 46 | Box( 47 | modifier = modifier 48 | ) { 49 | val scope = rememberCoroutineScope() 50 | val background = Modifier.background(MaterialTheme.colorScheme.background.copy(alpha = 0.75f),MaterialTheme.shapes.small) 51 | .border(width = 1.dp, color = MaterialTheme.colorScheme.outline, shape = MaterialTheme.shapes.small) 52 | .clip(MaterialTheme.shapes.small) 53 | val iconTint = MaterialTheme.colorScheme.onBackground 54 | 55 | HorizontalVueReader( 56 | modifier = Modifier.fillMaxSize(), 57 | contentModifier = Modifier.fillMaxSize(), 58 | horizontalVueReaderState = horizontalVueReaderState 59 | ) 60 | Row( 61 | modifier = Modifier 62 | .align(Alignment.TopCenter) 63 | .padding(horizontal = 8.dp, vertical = 12.dp), 64 | verticalAlignment = Alignment.CenterVertically 65 | ) { 66 | Text( 67 | text = "${horizontalVueReaderState.currentPage} of ${horizontalVueReaderState.pdfPageCount}", 68 | modifier = Modifier 69 | .then(background) 70 | .padding(10.dp) 71 | ) 72 | Spacer( 73 | modifier = Modifier 74 | .weight(1f) 75 | .fillMaxWidth() 76 | ) 77 | Row { 78 | val context = LocalContext.current 79 | IconButton( 80 | modifier = background, 81 | onClick = import 82 | ) { 83 | Icon( 84 | imageVector = Icons.Filled.Add, 85 | contentDescription = "Add Page", 86 | tint = iconTint 87 | ) 88 | } 89 | Spacer(modifier = Modifier.width(5.dp)) 90 | IconButton( 91 | modifier = background, 92 | onClick = { //Share 93 | horizontalVueReaderState.sharePDF(context) 94 | }) { 95 | Icon( 96 | imageVector = Icons.Filled.Share, 97 | contentDescription = "Share", 98 | tint = iconTint 99 | ) 100 | } 101 | } 102 | } 103 | Row( 104 | modifier = Modifier 105 | .align(Alignment.BottomCenter) 106 | .padding(horizontal = 8.dp, vertical = 12.dp), 107 | verticalAlignment = Alignment.CenterVertically 108 | ) { 109 | val showPrevious by remember { 110 | derivedStateOf { horizontalVueReaderState.currentPage != 1 } 111 | } 112 | val showNext by remember { 113 | derivedStateOf { horizontalVueReaderState.currentPage != horizontalVueReaderState.pdfPageCount } 114 | } 115 | if (showPrevious) 116 | IconButton( 117 | modifier = background, 118 | onClick = { 119 | //Prev 120 | scope.launch { 121 | horizontalVueReaderState.prevPage() 122 | } 123 | }) { 124 | Icon( 125 | imageVector = Icons.Filled.KeyboardArrowLeft, 126 | contentDescription = "Previous", 127 | tint = iconTint 128 | ) 129 | } 130 | else 131 | Spacer( 132 | modifier = Modifier 133 | .size(48.dp) 134 | .background(Color.Transparent) 135 | ) 136 | Spacer( 137 | modifier = Modifier 138 | .weight(1f) 139 | .fillMaxWidth() 140 | ) 141 | IconButton( 142 | modifier = background, 143 | onClick = { 144 | //Rotate 145 | horizontalVueReaderState.rotate(-90f) 146 | }) { 147 | Icon( 148 | imageVector = Icons.Filled.RotateLeft, 149 | contentDescription = "Rotate Left", 150 | tint = iconTint 151 | ) 152 | } 153 | Spacer(modifier = Modifier.width(5.dp)) 154 | IconButton( 155 | modifier = background, 156 | onClick = { 157 | //Rotate 158 | horizontalVueReaderState.rotate(90f) 159 | }) { 160 | Icon( 161 | imageVector = Icons.Filled.RotateRight, 162 | contentDescription = "Rotate Right", 163 | tint = iconTint 164 | ) 165 | } 166 | Spacer( 167 | modifier = Modifier 168 | .weight(1f) 169 | .fillMaxWidth() 170 | ) 171 | if (showNext) 172 | IconButton( 173 | modifier = background, 174 | onClick = { 175 | //Next 176 | scope.launch { 177 | horizontalVueReaderState.nextPage() 178 | } 179 | }) { 180 | Icon( 181 | imageVector = Icons.Filled.KeyboardArrowRight, 182 | contentDescription = "Next", 183 | tint = iconTint 184 | ) 185 | } 186 | else 187 | Spacer( 188 | modifier = Modifier 189 | .size(48.dp) 190 | .background(Color.Transparent) 191 | ) 192 | } 193 | } 194 | } -------------------------------------------------------------------------------- /app/src/main/java/com/pratikk/jetpackpdf/horizontalSamples/HorizontalSampleB.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpackpdf.horizontalSamples 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.border 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.BoxWithConstraints 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.Spacer 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.height 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.layout.width 15 | import androidx.compose.material.icons.Icons 16 | import androidx.compose.material.icons.filled.Add 17 | import androidx.compose.material.icons.filled.RotateLeft 18 | import androidx.compose.material.icons.filled.RotateRight 19 | import androidx.compose.material.icons.filled.Share 20 | import androidx.compose.material3.ExperimentalMaterial3Api 21 | import androidx.compose.material3.Icon 22 | import androidx.compose.material3.IconButton 23 | import androidx.compose.material3.MaterialTheme 24 | import androidx.compose.material3.Text 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.draw.clip 29 | import androidx.compose.ui.platform.LocalContext 30 | import androidx.compose.ui.unit.dp 31 | import com.pratikk.jetpdfvue.HorizontalVueReader 32 | import com.pratikk.jetpdfvue.VueHorizontalSlider 33 | import com.pratikk.jetpdfvue.state.HorizontalVueReaderState 34 | 35 | @OptIn(ExperimentalMaterial3Api::class) 36 | @Composable 37 | fun HorizontalSampleB( 38 | modifier: Modifier = Modifier, 39 | horizontalVueReaderState: HorizontalVueReaderState, 40 | import: () -> Unit 41 | ) { 42 | Box( 43 | modifier = modifier 44 | ) { 45 | val background = Modifier.background(MaterialTheme.colorScheme.background.copy(alpha = 0.75f),MaterialTheme.shapes.small) 46 | .border(width = 1.dp, color = MaterialTheme.colorScheme.outline, shape = MaterialTheme.shapes.small) 47 | .clip(MaterialTheme.shapes.small) 48 | val iconTint = MaterialTheme.colorScheme.onBackground 49 | 50 | HorizontalVueReader( 51 | modifier = Modifier.fillMaxSize(), 52 | contentModifier = Modifier.fillMaxSize(), 53 | horizontalVueReaderState = horizontalVueReaderState 54 | ) 55 | Row( 56 | modifier = Modifier 57 | .align(Alignment.TopCenter) 58 | .padding(horizontal = 8.dp, vertical = 12.dp), 59 | verticalAlignment = Alignment.CenterVertically 60 | ) { 61 | Text( 62 | text = "${horizontalVueReaderState.currentPage} of ${horizontalVueReaderState.pdfPageCount}", 63 | modifier = Modifier 64 | .then(background) 65 | .padding(10.dp) 66 | ) 67 | Spacer( 68 | modifier = Modifier 69 | .weight(1f) 70 | .fillMaxWidth() 71 | ) 72 | Row { 73 | val context = LocalContext.current 74 | IconButton( 75 | modifier = background, 76 | onClick = import 77 | ) { 78 | Icon( 79 | imageVector = Icons.Filled.Add, 80 | contentDescription = "Add Page", 81 | tint = iconTint 82 | ) 83 | } 84 | Spacer(modifier = Modifier.width(5.dp)) 85 | IconButton( 86 | modifier = background, 87 | onClick = { //Share 88 | horizontalVueReaderState.sharePDF(context) 89 | }) { 90 | Icon( 91 | imageVector = Icons.Filled.Share, 92 | contentDescription = "Share", 93 | tint = iconTint 94 | ) 95 | } 96 | } 97 | } 98 | Column( 99 | modifier = Modifier 100 | .align(Alignment.BottomCenter) 101 | .padding(horizontal = 8.dp, vertical = 12.dp) 102 | ) { 103 | Row( 104 | verticalAlignment = Alignment.CenterVertically 105 | ) { 106 | Spacer( 107 | modifier = Modifier 108 | .weight(1f) 109 | .fillMaxWidth() 110 | ) 111 | IconButton( 112 | modifier = background, 113 | onClick = { 114 | //Rotate 115 | horizontalVueReaderState.rotate(-90f) 116 | }) { 117 | Icon( 118 | imageVector = Icons.Filled.RotateLeft, 119 | contentDescription = "Rotate Left", 120 | tint = iconTint 121 | ) 122 | } 123 | Spacer(modifier = Modifier.width(5.dp)) 124 | IconButton( 125 | modifier = background, 126 | onClick = { 127 | //Rotate 128 | horizontalVueReaderState.rotate(90f) 129 | }) { 130 | Icon( 131 | imageVector = Icons.Filled.RotateRight, 132 | contentDescription = "Rotate Right", 133 | tint = iconTint 134 | ) 135 | } 136 | Spacer( 137 | modifier = Modifier 138 | .weight(1f) 139 | .fillMaxWidth() 140 | ) 141 | } 142 | VueHorizontalSlider( 143 | modifier = Modifier 144 | .padding(horizontal = 10.dp, vertical = 10.dp) 145 | .fillMaxWidth() 146 | .height(40.dp), 147 | horizontalVueReaderState = horizontalVueReaderState 148 | ) 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /app/src/main/java/com/pratikk/jetpackpdf/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpackpdf.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/com/pratikk/jetpackpdf/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpackpdf.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.WindowCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun JetpackPDFTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | 53 | darkTheme -> DarkColorScheme 54 | else -> LightColorScheme 55 | } 56 | val view = LocalView.current 57 | if (!view.isInEditMode) { 58 | SideEffect { 59 | val window = (view.context as Activity).window 60 | window.statusBarColor = colorScheme.primary.toArgb() 61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 62 | } 63 | } 64 | 65 | MaterialTheme( 66 | colorScheme = colorScheme, 67 | typography = Typography, 68 | content = content 69 | ) 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/pratikk/jetpackpdf/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpackpdf.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/pratikk/jetpackpdf/verticalSamples/VerticalPdfViewer.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpackpdf.verticalSamples 2 | 3 | import android.content.res.Configuration 4 | import android.net.Uri 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.BoxWithConstraints 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.fillMaxHeight 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.material3.Button 11 | import androidx.compose.material3.CircularProgressIndicator 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Surface 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.LaunchedEffect 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.runtime.rememberCoroutineScope 21 | import androidx.compose.runtime.saveable.rememberSaveable 22 | import androidx.compose.runtime.setValue 23 | import androidx.compose.ui.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.platform.LocalConfiguration 26 | import androidx.compose.ui.platform.LocalContext 27 | import androidx.compose.ui.unit.IntSize 28 | import androidx.core.net.toUri 29 | import com.pratikk.jetpdfvue.state.VerticalVueReaderState 30 | import com.pratikk.jetpdfvue.state.VueFilePicker 31 | import com.pratikk.jetpdfvue.state.VueFilePickerState 32 | import com.pratikk.jetpdfvue.state.VueImportSources 33 | import com.pratikk.jetpdfvue.state.VueFileType 34 | import com.pratikk.jetpdfvue.state.VueLoadState 35 | import com.pratikk.jetpdfvue.state.VueResourceType 36 | import com.pratikk.jetpdfvue.state.rememberVerticalVueReaderState 37 | import com.pratikk.jetpdfvue.util.compressImageToThreshold 38 | import com.pratikk.jetpdfvue.util.getFileType 39 | import kotlinx.coroutines.launch 40 | 41 | @Composable 42 | fun VerticalPdfViewerLocal(){ 43 | Surface( 44 | modifier = Modifier 45 | .fillMaxSize(), 46 | color = MaterialTheme.colorScheme.background 47 | ) { 48 | val context = LocalContext.current 49 | val vueFilePicker = rememberSaveable(saver = VueFilePicker.Saver) { 50 | VueFilePicker() 51 | } 52 | var uri: Uri by rememberSaveable(stateSaver = VueFilePicker.UriSaver) { 53 | mutableStateOf(Uri.EMPTY) 54 | } 55 | val launcher = vueFilePicker.getLauncher(onResult = { 56 | uri = it.toUri() 57 | }) 58 | if(uri == Uri.EMPTY){ 59 | Column(modifier = Modifier.fillMaxSize(), 60 | horizontalAlignment = Alignment.CenterHorizontally, 61 | verticalArrangement = Arrangement.Center) { 62 | Button(onClick = { vueFilePicker.launchIntent(context, listOf( 63 | VueImportSources.CAMERA, 64 | VueImportSources.GALLERY, 65 | VueImportSources.PDF),launcher)}) { 66 | Text(text = "Import Document") 67 | } 68 | } 69 | }else{ 70 | val localImage = rememberVerticalVueReaderState( 71 | resource = VueResourceType.Local( 72 | uri = uri, 73 | fileType = uri.getFileType(context) 74 | ) 75 | ) 76 | VerticalPdfViewer(verticalVueReaderState = localImage) 77 | } 78 | } 79 | } 80 | @Composable 81 | fun VerticalPdfViewer(verticalVueReaderState: VerticalVueReaderState) { 82 | val context = LocalContext.current 83 | val scope = rememberCoroutineScope() 84 | 85 | val launcher = verticalVueReaderState.getImportLauncher(interceptResult = { 86 | it.compressImageToThreshold(2) 87 | }) 88 | 89 | BoxWithConstraints( 90 | modifier = Modifier, 91 | contentAlignment = Alignment.Center 92 | ) { 93 | val configuration = LocalConfiguration.current 94 | val containerSize = remember { 95 | IntSize(constraints.maxWidth, constraints.maxHeight) 96 | } 97 | 98 | LaunchedEffect(Unit) { 99 | verticalVueReaderState.load( 100 | context = context, 101 | coroutineScope = scope, 102 | containerSize = containerSize, 103 | isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT, 104 | customResource = null 105 | ) 106 | } 107 | when (verticalVueReaderState.vueLoadState) { 108 | is VueLoadState.NoDocument -> { 109 | Button(onClick = { 110 | verticalVueReaderState.launchImportIntent( 111 | context = context, 112 | launcher = launcher 113 | ) 114 | }) { 115 | Text(text = "Import Document") 116 | } 117 | } 118 | 119 | is VueLoadState.DocumentError -> { 120 | Column { 121 | Text(text = "Error: ${verticalVueReaderState.vueLoadState.getErrorMessage}") 122 | Button(onClick = { 123 | scope.launch { 124 | verticalVueReaderState.load( 125 | context = context, 126 | coroutineScope = scope, 127 | containerSize = containerSize, 128 | isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT, 129 | customResource = null 130 | ) 131 | } 132 | }) { 133 | Text(text = "Retry") 134 | } 135 | } 136 | } 137 | 138 | is VueLoadState.DocumentImporting -> { 139 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 140 | CircularProgressIndicator() 141 | Text(text = "Importing...") 142 | } 143 | } 144 | 145 | is VueLoadState.DocumentLoaded -> { 146 | VerticalSampleA( 147 | modifier = Modifier.fillMaxHeight(), 148 | verticalVueReaderState = verticalVueReaderState) { 149 | verticalVueReaderState.launchImportIntent( 150 | context = context, 151 | launcher = launcher 152 | ) 153 | } 154 | 155 | } 156 | 157 | is VueLoadState.DocumentLoading -> { 158 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 159 | CircularProgressIndicator() 160 | Text(text = "Loading ${verticalVueReaderState.loadPercent}") 161 | } 162 | } 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /app/src/main/java/com/pratikk/jetpackpdf/verticalSamples/VerticalSampleA.kt: -------------------------------------------------------------------------------- 1 | package com.pratikk.jetpackpdf.verticalSamples 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.border 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.BoxWithConstraints 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.layout.width 14 | import androidx.compose.material.icons.Icons 15 | import androidx.compose.material.icons.filled.Add 16 | import androidx.compose.material.icons.filled.KeyboardArrowDown 17 | import androidx.compose.material.icons.filled.KeyboardArrowUp 18 | import androidx.compose.material.icons.filled.RotateLeft 19 | import androidx.compose.material.icons.filled.RotateRight 20 | import androidx.compose.material.icons.filled.Share 21 | import androidx.compose.material3.Icon 22 | import androidx.compose.material3.IconButton 23 | import androidx.compose.material3.MaterialTheme 24 | import androidx.compose.material3.Text 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.runtime.derivedStateOf 27 | import androidx.compose.runtime.getValue 28 | import androidx.compose.runtime.remember 29 | import androidx.compose.runtime.rememberCoroutineScope 30 | import androidx.compose.ui.Alignment 31 | import androidx.compose.ui.Modifier 32 | import androidx.compose.ui.draw.clip 33 | import androidx.compose.ui.graphics.Color 34 | import androidx.compose.ui.platform.LocalContext 35 | import androidx.compose.ui.unit.dp 36 | import com.pratikk.jetpdfvue.VerticalVueReader 37 | import com.pratikk.jetpdfvue.state.VerticalVueReaderState 38 | import kotlinx.coroutines.launch 39 | 40 | 41 | @Composable 42 | fun VerticalSampleA( 43 | modifier: Modifier = Modifier, 44 | verticalVueReaderState: VerticalVueReaderState, 45 | import:() -> Unit 46 | ){ 47 | Box( 48 | modifier = modifier 49 | ) { 50 | val scope = rememberCoroutineScope() 51 | 52 | val background = Modifier.background(MaterialTheme.colorScheme.background.copy(alpha = 0.75f),MaterialTheme.shapes.small) 53 | .border(width = 1.dp, color = MaterialTheme.colorScheme.outline, shape = MaterialTheme.shapes.small) 54 | .clip(MaterialTheme.shapes.small) 55 | val iconTint = MaterialTheme.colorScheme.onBackground 56 | 57 | VerticalVueReader( 58 | modifier = Modifier.fillMaxSize(), 59 | contentModifier = Modifier.fillMaxSize(), 60 | verticalVueReaderState = verticalVueReaderState 61 | ) 62 | Row( 63 | modifier = Modifier 64 | .align(Alignment.TopCenter) 65 | .padding(horizontal = 8.dp, vertical = 12.dp), 66 | verticalAlignment = Alignment.CenterVertically 67 | ) { 68 | Text( 69 | text = "${verticalVueReaderState.currentPage} of ${verticalVueReaderState.pdfPageCount}", 70 | modifier = Modifier 71 | .then(background) 72 | .padding(10.dp) 73 | ) 74 | Spacer( 75 | modifier = Modifier 76 | .weight(1f) 77 | .fillMaxWidth() 78 | ) 79 | Row { 80 | val context = LocalContext.current 81 | IconButton( 82 | modifier = background, 83 | onClick = import 84 | ) { 85 | Icon( 86 | imageVector = Icons.Filled.Add, 87 | contentDescription = "Add Page", 88 | tint = iconTint 89 | ) 90 | } 91 | Spacer(modifier = Modifier.width(5.dp)) 92 | IconButton( 93 | modifier = background, 94 | onClick = { //Share 95 | verticalVueReaderState.sharePDF(context) 96 | }) { 97 | Icon( 98 | imageVector = Icons.Filled.Share, 99 | contentDescription = "Share", 100 | tint = iconTint 101 | ) 102 | } 103 | } 104 | } 105 | Row( 106 | modifier = Modifier 107 | .align(Alignment.BottomCenter) 108 | .padding(horizontal = 8.dp, vertical = 12.dp), 109 | verticalAlignment = Alignment.CenterVertically 110 | ) { 111 | val showPrevious by remember { 112 | derivedStateOf { verticalVueReaderState.currentPage != 1 } 113 | } 114 | val showNext by remember { 115 | derivedStateOf { verticalVueReaderState.currentPage != verticalVueReaderState.pdfPageCount } 116 | } 117 | if (showPrevious) 118 | IconButton( 119 | modifier = background, 120 | onClick = { 121 | //Prev 122 | scope.launch { 123 | verticalVueReaderState.prevPage() 124 | } 125 | }) { 126 | Icon( 127 | imageVector = Icons.Filled.KeyboardArrowUp, 128 | contentDescription = "Previous", 129 | tint = iconTint 130 | ) 131 | } 132 | else 133 | Spacer( 134 | modifier = Modifier 135 | .size(48.dp) 136 | .background(Color.Transparent) 137 | ) 138 | Spacer( 139 | modifier = Modifier 140 | .weight(1f) 141 | .fillMaxWidth() 142 | ) 143 | IconButton( 144 | modifier = background, 145 | onClick = { 146 | //Rotate 147 | verticalVueReaderState.rotate(-90f) 148 | }) { 149 | Icon( 150 | imageVector = Icons.Filled.RotateLeft, 151 | contentDescription = "Rotate Left", 152 | tint = iconTint 153 | ) 154 | } 155 | Spacer(modifier = Modifier.width(5.dp)) 156 | IconButton( 157 | modifier = background, 158 | onClick = { 159 | //Rotate 160 | verticalVueReaderState.rotate(90f) 161 | }) { 162 | Icon( 163 | imageVector = Icons.Filled.RotateRight, 164 | contentDescription = "Rotate Right", 165 | tint = iconTint 166 | ) 167 | } 168 | Spacer( 169 | modifier = Modifier 170 | .weight(1f) 171 | .fillMaxWidth() 172 | ) 173 | if (showNext) 174 | IconButton( 175 | modifier = background, 176 | onClick = { 177 | //Next 178 | scope.launch { 179 | verticalVueReaderState.nextPage() 180 | } 181 | }) { 182 | Icon( 183 | imageVector = Icons.Filled.KeyboardArrowDown, 184 | contentDescription = "Next", 185 | tint = iconTint 186 | ) 187 | } 188 | else 189 | Spacer( 190 | modifier = Modifier 191 | .size(48.dp) 192 | .background(Color.Transparent) 193 | ) 194 | } 195 | } 196 | } -------------------------------------------------------------------------------- /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_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratikksahu/JetPDFVue/c763f7b84fc44ac336b4af697911409a75aea009/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratikksahu/JetPDFVue/c763f7b84fc44ac336b4af697911409a75aea009/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratikksahu/JetPDFVue/c763f7b84fc44ac336b4af697911409a75aea009/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratikksahu/JetPDFVue/c763f7b84fc44ac336b4af697911409a75aea009/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratikksahu/JetPDFVue/c763f7b84fc44ac336b4af697911409a75aea009/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratikksahu/JetPDFVue/c763f7b84fc44ac336b4af697911409a75aea009/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratikksahu/JetPDFVue/c763f7b84fc44ac336b4af697911409a75aea009/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratikksahu/JetPDFVue/c763f7b84fc44ac336b4af697911409a75aea009/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratikksahu/JetPDFVue/c763f7b84fc44ac336b4af697911409a75aea009/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratikksahu/JetPDFVue/c763f7b84fc44ac336b4af697911409a75aea009/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/raw/demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratikksahu/JetPDFVue/c763f7b84fc44ac336b4af697911409a75aea009/app/src/main/res/raw/demo.jpg -------------------------------------------------------------------------------- /app/src/main/res/raw/lorem_ipsum.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratikksahu/JetPDFVue/c763f7b84fc44ac336b4af697911409a75aea009/app/src/main/res/raw/lorem_ipsum.pdf -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | JetpackPDF 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |