Rely on PDFBox-Android to reduce the size of generated PDFs

This commit is contained in:
Pierre-Yves Nicolas
2025-06-04 18:22:57 +02:00
parent ebd5453b65
commit 6a90723fb3
5 changed files with 31 additions and 26 deletions

View File

@@ -60,6 +60,7 @@ dependencies {
implementation(libs.litert.support) implementation(libs.litert.support)
implementation(libs.litert.metadata) implementation(libs.litert.metadata)
implementation(libs.opencv) implementation(libs.opencv)
implementation(libs.pdfbox)
testImplementation(libs.junit) testImplementation(libs.junit)
testImplementation(libs.assertj) testImplementation(libs.assertj)

View File

@@ -2,7 +2,6 @@ package org.mydomain.myscan
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
@@ -32,7 +31,7 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
initOpenCV() initLibraries()
val viewModel: MainViewModel by viewModels { MainViewModel.getFactory(this) } val viewModel: MainViewModel by viewModels { MainViewModel.getFactory(this) }
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
@@ -68,19 +67,15 @@ class MainActivity : ComponentActivity() {
viewModel: MainViewModel, viewModel: MainViewModel,
context: Context context: Context
): () -> Unit = { ): () -> Unit = {
val document = viewModel.createPdf()
val outputDir = File(cacheDir, "pdfs").apply { mkdirs() } val outputDir = File(cacheDir, "pdfs").apply { mkdirs() }
val outputFile = File(outputDir, "scan_${System.currentTimeMillis()}.pdf") val outputFile = File(outputDir, "scan_${System.currentTimeMillis()}.pdf")
var success = true var success = true
try { try {
FileOutputStream(outputFile).use { outputStream -> val fileOutputStream = FileOutputStream(outputFile)
document.writeTo(outputStream) viewModel.createPdf(fileOutputStream)
}
} catch (_: IOException) { } catch (_: IOException) {
Toast.makeText(context, "Failed to share PDF", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Failed to share PDF", Toast.LENGTH_SHORT).show()
success = false success = false
} finally {
document.close()
} }
if (success) { if (success) {
val uri = FileProvider.getUriForFile( val uri = FileProvider.getUriForFile(
@@ -101,13 +96,12 @@ class MainActivity : ComponentActivity() {
viewModel: MainViewModel, viewModel: MainViewModel,
context: Context context: Context
): () -> Unit = { ): () -> Unit = {
val document = viewModel.createPdf()
try { try {
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
if (!downloadsDir.exists()) downloadsDir.mkdirs() if (!downloadsDir.exists()) downloadsDir.mkdirs()
val file = File(downloadsDir, "scan_${System.currentTimeMillis()}.pdf") val file = File(downloadsDir, "scan_${System.currentTimeMillis()}.pdf")
val outputStream = FileOutputStream(file) val outputStream = FileOutputStream(file)
document.writeTo(outputStream) viewModel.createPdf(outputStream)
outputStream.flush() outputStream.flush()
outputStream.close() outputStream.close()
@@ -119,12 +113,12 @@ class MainActivity : ComponentActivity() {
} catch (e: Exception) { } catch (e: Exception) {
Log.e("MyScan", "Failed to save PDF", e) Log.e("MyScan", "Failed to save PDF", e)
Toast.makeText(context, "Failed to save PDF", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Failed to save PDF", Toast.LENGTH_SHORT).show()
} finally {
document.close()
} }
} }
private fun initOpenCV() { private fun initLibraries() {
com.tom_roush.pdfbox.android.PDFBoxResourceLoader.init(applicationContext)
if (!OpenCVLoader.initLocal()) { if (!OpenCVLoader.initLocal()) {
Log.e("OpenCV", "Initialization failed") Log.e("OpenCV", "Initialization failed")
} else { } else {

View File

@@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.OutputStream
class MainViewModel( class MainViewModel(
private val imageSegmentationService: ImageSegmentationService, private val imageSegmentationService: ImageSegmentationService,
@@ -128,9 +129,9 @@ class MainViewModel(
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size) return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
} }
fun createPdf(): PdfDocument { fun createPdf(outputStream: OutputStream) {
val jpegs = imageRepository.imageIds().asSequence() val jpegs = imageRepository.imageIds().asSequence()
.map { id -> imageRepository.getContent(id) } .map { id -> imageRepository.getContent(id) }
return createPdfFromJpegs(jpegs) writePdfFromJpegs(jpegs, outputStream)
} }
} }

View File

@@ -1,21 +1,28 @@
package org.mydomain.myscan package org.mydomain.myscan
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.pdf.PdfDocument
import androidx.core.graphics.scale import androidx.core.graphics.scale
import com.tom_roush.pdfbox.pdmodel.PDDocument
import com.tom_roush.pdfbox.pdmodel.PDPage
import com.tom_roush.pdfbox.pdmodel.PDPageContentStream
import com.tom_roush.pdfbox.pdmodel.PDPageContentStream.AppendMode
import com.tom_roush.pdfbox.pdmodel.common.PDRectangle
import com.tom_roush.pdfbox.pdmodel.graphics.image.JPEGFactory
import java.io.OutputStream
import kotlin.math.max import kotlin.math.max
fun createPdfFromJpegs (jpegs: Sequence<ByteArray>): PdfDocument { fun writePdfFromJpegs(jpegs: Sequence<ByteArray>, outputStream: OutputStream) {
val document = PdfDocument() PDDocument().use { document ->
for ((index, jpegBytes) in jpegs.withIndex()) { for (jpegBytes in jpegs) {
val bitmap = BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size) val image = JPEGFactory.createFromByteArray(document, jpegBytes)
val pageInfo = PdfDocument.PageInfo.Builder(bitmap.width, bitmap.height, index + 1).create() val page = PDPage(PDRectangle(image.width.toFloat(), image.height.toFloat()))
val page = document.startPage(pageInfo) document.addPage(page)
page.canvas.drawBitmap(bitmap, 0f, 0f, null) val contentStream = PDPageContentStream(document, page, AppendMode.OVERWRITE, false)
document.finishPage(page) contentStream.drawImage(image, 0f, 0f)
contentStream.close()
}
document.save(outputStream)
} }
return document
} }
fun resizeImage(original: Bitmap): Bitmap { fun resizeImage(original: Bitmap): Bitmap {

View File

@@ -13,6 +13,7 @@ litert = "1.2.0"
opencv = "4.11.0" opencv = "4.11.0"
flowlayout = "0.36.0" flowlayout = "0.36.0"
assertj = "3.27.3" assertj = "3.27.3"
pdfbox = "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" }
@@ -41,6 +42,7 @@ litert = { group = "com.google.ai.edge.litert", name = "litert", version.ref = "
litert-support = { group = "com.google.ai.edge.litert", name = "litert-support", version.ref = "litert" } litert-support = { group = "com.google.ai.edge.litert", name = "litert-support", version.ref = "litert" }
litert-metadata = { group = "com.google.ai.edge.litert", name = "litert-metadata", version.ref = "litert" } litert-metadata = { group = "com.google.ai.edge.litert", name = "litert-metadata", version.ref = "litert" }
opencv = { group="org.opencv", name="opencv", version.ref = "opencv" } opencv = { group="org.opencv", name="opencv", version.ref = "opencv" }
pdfbox = { group = "com.tom-roush", name = "pdfbox-android", version.ref = "pdfbox" }
assertj = { group="org.assertj", name="assertj-core", version.ref = "assertj" } assertj = { group="org.assertj", name="assertj-core", version.ref = "assertj" }