Add PdfBox library for future parsing

Change buttons to chips
This commit is contained in:
Luke Hubmayer-Werner 2024-08-24 18:10:26 +09:30
parent df3b9ac93b
commit 04a64bd621
6 changed files with 220 additions and 119 deletions

View File

@ -51,4 +51,6 @@ dependencies {
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
implementation(libs.pdfbox.android)
} }

View File

@ -11,6 +11,8 @@ import android.view.Display
import android.view.Menu import android.view.Menu
import android.view.WindowManager import android.view.WindowManager
import android.widget.ImageView import android.widget.ImageView
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import androidx.navigation.findNavController import androidx.navigation.findNavController
@ -23,6 +25,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import com.lhw.pdf.databinding.ActivityMainBinding import com.lhw.pdf.databinding.ActivityMainBinding
import com.tom_roush.pdfbox.android.PDFBoxResourceLoader
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.InputStream import java.io.InputStream
@ -37,6 +40,11 @@ class MainActivity : AppCompatActivity() {
private val thumbnailHeight = 700 private val thumbnailHeight = 700
private var renderAutoCrop = true private var renderAutoCrop = true
private var pagesPerLandscape = 3F private var pagesPerLandscape = 3F
private set(pages) {
field = pages
updatePresentations()
}
private var usePdfBox = false
private val presentations = mutableMapOf<Int, MyPresentation>() private val presentations = mutableMapOf<Int, MyPresentation>()
private lateinit var pdfDocument: PdfDocument private lateinit var pdfDocument: PdfDocument
private val showPages = mutableListOf<Boolean>() private val showPages = mutableListOf<Boolean>()
@ -151,12 +159,15 @@ class MainActivity : AppCompatActivity() {
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
//enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
//val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView) //val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
//windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) //Doesn't seem to actually fill the vacant space by default //windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) //Doesn't seem to actually fill the vacant space by default
PDFBoxResourceLoader.init(this)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@ -169,7 +180,7 @@ class MainActivity : AppCompatActivity() {
if (!file.exists()) { if (!file.exists()) {
file = inputStreamToCache(defaultCachedFileName, resources.openRawResource(R.raw.testpdf)) file = inputStreamToCache(defaultCachedFileName, resources.openRawResource(R.raw.testpdf))
} }
pdfDocument = PdfDocument(file, true) pdfDocument = PdfDocument(file, usePdfBox = false, autoCrop = true)
// Then overwrite it with any pdf sent via intent // Then overwrite it with any pdf sent via intent
handleIntent(intent) handleIntent(intent)
@ -184,26 +195,24 @@ class MainActivity : AppCompatActivity() {
presentations.values.forEach {it.setScrollProgress(progress)} presentations.values.forEach {it.setScrollProgress(progress)}
} }
binding.appBarMain.crop.setOnClickListener { view -> binding.appBarMain.contentMain.chipPages1.setOnCheckedChangeListener { _, checked -> if (checked) pagesPerLandscape = 1F }
renderAutoCrop = !renderAutoCrop binding.appBarMain.contentMain.chipPages15.setOnCheckedChangeListener { _, checked -> if (checked) pagesPerLandscape = 1.5F }
val s = if (renderAutoCrop) "On" else "Off" binding.appBarMain.contentMain.chipPages2.setOnCheckedChangeListener { _, checked -> if (checked) pagesPerLandscape = 2F }
Snackbar.make(view, "Toggling Auto Crop $s", Snackbar.LENGTH_LONG) binding.appBarMain.contentMain.chipPages25.setOnCheckedChangeListener { _, checked -> if (checked) pagesPerLandscape = 2.5F }
.setAction("Action", null) binding.appBarMain.contentMain.chipPages3.setOnCheckedChangeListener { _, checked -> if (checked) pagesPerLandscape = 3F }
.setAnchorView(R.id.crop).show() binding.appBarMain.contentMain.chipPages35.setOnCheckedChangeListener { _, checked -> if (checked) pagesPerLandscape = 3.5F }
binding.appBarMain.contentMain.chipPages4.setOnCheckedChangeListener { _, checked -> if (checked) pagesPerLandscape = 4F }
binding.appBarMain.contentMain.chipPages5.setOnCheckedChangeListener { _, checked -> if (checked) pagesPerLandscape = 5F }
binding.appBarMain.contentMain.chipPages6.setOnCheckedChangeListener { _, checked -> if (checked) pagesPerLandscape = 6F }
binding.appBarMain.contentMain.chipPages8.setOnCheckedChangeListener { _, checked -> if (checked) pagesPerLandscape = 8F }
binding.appBarMain.contentMain.chipPages10.setOnCheckedChangeListener { _, checked -> if (checked) pagesPerLandscape = 10F }
binding.appBarMain.contentMain.chipAutoCrop.setOnCheckedChangeListener { _, checked ->
renderAutoCrop = checked
updatePresentations() updatePresentations()
} }
binding.appBarMain.zoomIn.setOnClickListener { view -> binding.appBarMain.contentMain.chipUsePdfBox.setOnCheckedChangeListener { _, checked ->
pagesPerLandscape = max(pagesPerLandscape - 0.5F, 1F) usePdfBox = checked
Snackbar.make(view, "Aiming for $pagesPerLandscape pages at a time", Snackbar.LENGTH_LONG) pdfDocument.usePdfBox = usePdfBox
.setAction("Action", null)
.setAnchorView(R.id.zoom_in).show()
updatePresentations()
}
binding.appBarMain.zoomOut.setOnClickListener { view ->
pagesPerLandscape = min(pagesPerLandscape + 0.5F, 10F)
Snackbar.make(view, "Aiming for $pagesPerLandscape pages at a time", Snackbar.LENGTH_LONG)
.setAction("Action", null)
.setAnchorView(R.id.zoom_out).show()
updatePresentations() updatePresentations()
} }
val drawerLayout: DrawerLayout = binding.drawerLayout val drawerLayout: DrawerLayout = binding.drawerLayout

View File

@ -7,25 +7,40 @@ import android.graphics.Rect
import android.graphics.RectF import android.graphics.RectF
import android.graphics.pdf.PdfRenderer import android.graphics.pdf.PdfRenderer
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import com.tom_roush.pdfbox.pdmodel.PDDocument
import com.tom_roush.pdfbox.rendering.PDFRenderer
import java.io.File import java.io.File
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
class PdfDocument(private val fileCached: File, private val autoCrop: Boolean = false) { class PdfDocument(private val fileCached: File, var usePdfBox: Boolean = true, private val autoCrop: Boolean = false) {
// Stock Android PdfRenderer - fast but limited
private val pfd = ParcelFileDescriptor.open(fileCached, ParcelFileDescriptor.MODE_READ_ONLY) private val pfd = ParcelFileDescriptor.open(fileCached, ParcelFileDescriptor.MODE_READ_ONLY)
private val renderer = PdfRenderer(pfd) private val renderer = PdfRenderer(pfd)
val bitmapThumbnails = mutableMapOf<Int, Bitmap>()
val bitmapPages = mutableMapOf<Pair<Int, Int>, MutableMap<Int, Bitmap>>() // bitmapPages[<maxWidthPx, maxHeightPx>][pageId]
private val pagePtWidths = mutableListOf<Int>() private val pagePtWidths = mutableListOf<Int>()
private val pagePtHeights = mutableListOf<Int>() private val pagePtHeights = mutableListOf<Int>()
// PdfBox parser/renderer - powerful but slow
private val pdfDocument = PDDocument.load(fileCached)
private val boxRenderer = PDFRenderer(pdfDocument)
private val boxPagePtWidths = mutableListOf<Float>()
private val boxPagePtHeights = mutableListOf<Float>()
val bitmapThumbnails = mutableMapOf<Int, Bitmap>()
val bitmapPages = mutableMapOf<Pair<Int, Int>, MutableMap<Int, Bitmap>>() // bitmapPages[<maxWidthPx, maxHeightPx>][pageId]
private val pageAutoCropRects = mutableListOf<RectF>() private val pageAutoCropRects = mutableListOf<RectF>()
// Due to the way thumbnails render, the bottom right is more likely to be cut off // Due to the way thumbnails render, the bottom right is more likely to be cut off
private val autoCropSafetyMarginUpper = 2 private val autoCropSafetyMarginUpper = 2
private val autoCropSafetyMarginLower = 16 private val autoCropSafetyMarginLower = 16
private val numPages get() = if (usePdfBox) pdfDocument.numberOfPages else renderer.pageCount
init { init {
val numPages = renderer.pageCount
for (i in 0..<numPages) { for (i in 0..<numPages) {
// PdfBox
val bBox = pdfDocument.getPage(i).bBox
boxPagePtWidths.add(bBox.width)
boxPagePtHeights.add(bBox.height)
// Android
val page = renderer.openPage(i) val page = renderer.openPage(i)
pagePtWidths.add(page.width) pagePtWidths.add(page.width)
pagePtHeights.add(page.height) pagePtHeights.add(page.height)
@ -33,6 +48,7 @@ class PdfDocument(private val fileCached: File, private val autoCrop: Boolean =
} }
} }
private val rectFIdentity = RectF(0F, 0F, 1F, 1F) private val rectFIdentity = RectF(0F, 0F, 1F, 1F)
private fun renderPage(index: Int, maxWidth: Int, maxHeight: Int, crop: Boolean = false): Bitmap { private fun renderPage(index: Int, maxWidth: Int, maxHeight: Int, crop: Boolean = false): Bitmap {
val cropRect = if (!crop || pageAutoCropRects.size <= index) rectFIdentity else pageAutoCropRects[index] val cropRect = if (!crop || pageAutoCropRects.size <= index) rectFIdentity else pageAutoCropRects[index]
@ -42,9 +58,14 @@ class PdfDocument(private val fileCached: File, private val autoCrop: Boolean =
val heightFromMaxWidth = (ptHeight * maxWidth/ptWidth).toInt() val heightFromMaxWidth = (ptHeight * maxWidth/ptWidth).toInt()
val (width, height) = if (widthFromMaxHeight > maxWidth) Pair(maxWidth, heightFromMaxWidth) else Pair(widthFromMaxHeight, maxHeight) val (width, height) = if (widthFromMaxHeight > maxWidth) Pair(maxWidth, heightFromMaxWidth) else Pair(widthFromMaxHeight, maxHeight)
val bitmap: Bitmap
if (usePdfBox) {
val scale = width / ptWidth
bitmap = boxRenderer.renderImage(index, scale)
} else {
val page = renderer.openPage(index) val page = renderer.openPage(index)
println("Creating page $index bitmap with width $width and height $height from cropRect $cropRect") println("Creating page $index bitmap with width $width and height $height from cropRect $cropRect")
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val ptToPixel = height / ptHeight val ptToPixel = height / ptHeight
val transform = Matrix() val transform = Matrix()
val transposePtX = -cropRect.left * ptWidth val transposePtX = -cropRect.left * ptWidth
@ -52,12 +73,12 @@ class PdfDocument(private val fileCached: File, private val autoCrop: Boolean =
transform.setValues(floatArrayOf(ptToPixel, 0F, transposePtX * ptToPixel, 0F, ptToPixel, transposePtY * ptToPixel, 0F, 0F, 1F)) transform.setValues(floatArrayOf(ptToPixel, 0F, transposePtX * ptToPixel, 0F, ptToPixel, transposePtY * ptToPixel, 0F, 0F, 1F))
page.render(bitmap, null, transform, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) page.render(bitmap, null, transform, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
page.close() page.close()
}
return bitmap return bitmap
} }
private fun renderPagesToMap(map: MutableMap<Int, Bitmap>, width: Int, height: Int, crop: Boolean = false, overwrite: Boolean = true) { private fun renderPagesToMap(map: MutableMap<Int, Bitmap>, width: Int, height: Int, crop: Boolean = false, overwrite: Boolean = true) {
if (overwrite) map.clear() if (overwrite) map.clear()
val numPages = renderer.pageCount
for (i in 0..<numPages) { for (i in 0..<numPages) {
map.computeIfAbsent(i) { renderPage(i, width, height, crop) } map.computeIfAbsent(i) { renderPage(i, width, height, crop) }
} }
@ -72,7 +93,6 @@ class PdfDocument(private val fileCached: File, private val autoCrop: Boolean =
println("Thumbnails rendered") println("Thumbnails rendered")
if (autoCrop) { if (autoCrop) {
println("Auto cropping from thumbnails") println("Auto cropping from thumbnails")
val numPages = renderer.pageCount
pageAutoCropRects.clear() pageAutoCropRects.clear()
for (i in 0..<numPages) { for (i in 0..<numPages) {
println("Auto cropping page $i from thumbnail") println("Auto cropping page $i from thumbnail")

View File

@ -22,39 +22,4 @@
<include android:id="@+id/content_main" layout="@layout/content_main" /> <include android:id="@+id/content_main" layout="@layout/content_main" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/crop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/fab_margin"
android:layout_marginBottom="16dp"
android:importantForAccessibility="no"
app:maxImageSize="48dp"
app:srcCompat="@android:drawable/ic_menu_crop" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/zoom_in"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:layout_marginStart="96dp"
android:layout_marginBottom="16dp"
android:importantForAccessibility="no"
app:maxImageSize="48dp"
app:srcCompat="@android:drawable/ic_menu_zoom" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/zoom_out"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:layout_marginStart="16dp"
android:layout_marginBottom="16dp"
android:importantForAccessibility="no"
android:rotationY="180"
android:rotationX="180"
app:maxImageSize="48dp"
app:srcCompat="@android:drawable/ic_menu_zoom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -18,16 +18,16 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" /> app:navGraph="@navigation/mobile_navigation" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<HorizontalScrollView <HorizontalScrollView
android:id="@+id/thumbnails_disabled_scroll" android:id="@+id/thumbnails_disabled_scroll"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="300dp" android:layout_height="300dp"
android:background="#80C01010" android:background="#80C01010">
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0">
<LinearLayout <LinearLayout
android:id="@+id/thumbnails_disabled_layout" android:id="@+id/thumbnails_disabled_layout"
@ -39,13 +39,9 @@
<HorizontalScrollView <HorizontalScrollView
android:id="@+id/thumbnails_scroll" android:id="@+id/thumbnails_scroll"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="300dp" android:layout_weight="3">
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout <LinearLayout
android:id="@+id/thumbnails_layout" android:id="@+id/thumbnails_layout"
@ -60,12 +56,119 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="308dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:textAlignment="center" android:textAlignment="center"
android:textSize="20sp" android:textSize="20sp" />
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" <com.google.android.material.chip.ChipGroup
app:layout_constraintStart_toStartOf="parent" android:id="@+id/chip_group_pages"
app:layout_constraintTop_toTopOf="parent" /> app:singleSelection="true"
app:selectionRequired="true"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<com.google.android.material.chip.Chip
android:id="@+id/chip_pages_1"
android:checkable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1 page" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_pages_1.5"
android:checkable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1.5 pages" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_pages_2"
android:checkable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="2 pages" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_pages_2.5"
android:checkable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="2.5 pages" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_pages_3"
android:checkable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="3 pages" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_pages_3.5"
android:checkable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="3.5 pages" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_pages_4"
android:checkable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="4 pages" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_pages_5"
android:checkable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="5 pages" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_pages_6"
android:checkable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="6 pages" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_pages_8"
android:checkable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="8 pages" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_pages_10"
android:checkable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="10 pages" />
</com.google.android.material.chip.ChipGroup>
<com.google.android.material.chip.ChipGroup
android:id="@+id/chip_group_toggles"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<com.google.android.material.chip.Chip
android:id="@+id/chip_auto_crop"
android:checkable="true"
android:checked="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Auto Crop" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_use_pdf_box"
android:checkable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Use PdfBox renderer (slower)" />
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -12,6 +12,7 @@ lifecycleLivedataKtx = "2.8.4"
lifecycleViewmodelKtx = "2.8.4" lifecycleViewmodelKtx = "2.8.4"
navigationFragmentKtx = "2.7.7" navigationFragmentKtx = "2.7.7"
navigationUiKtx = "2.7.7" navigationUiKtx = "2.7.7"
pdfboxAndroid = "2.0.27.0"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -25,6 +26,7 @@ androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecy
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" } androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" } androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" } androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" }
pdfbox-android = { module = "com.tom-roush:pdfbox-android", version.ref = "pdfboxAndroid" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }