From 6a90723fb32118bf7f7e8d8bf1a6867528a12f75 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Nicolas <6371790+pynicolas@users.noreply.github.com> Date: Wed, 4 Jun 2025 18:22:57 +0200 Subject: [PATCH] Rely on PDFBox-Android to reduce the size of generated PDFs --- app/build.gradle.kts | 1 + .../java/org/mydomain/myscan/MainActivity.kt | 20 +++++-------- .../java/org/mydomain/myscan/MainViewModel.kt | 5 ++-- .../java/org/mydomain/myscan/PdfGeneration.kt | 29 ++++++++++++------- gradle/libs.versions.toml | 2 ++ 5 files changed, 31 insertions(+), 26 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b9b1432..7b865c1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -60,6 +60,7 @@ dependencies { implementation(libs.litert.support) implementation(libs.litert.metadata) implementation(libs.opencv) + implementation(libs.pdfbox) testImplementation(libs.junit) testImplementation(libs.assertj) diff --git a/app/src/main/java/org/mydomain/myscan/MainActivity.kt b/app/src/main/java/org/mydomain/myscan/MainActivity.kt index 7158f16..ffd3648 100644 --- a/app/src/main/java/org/mydomain/myscan/MainActivity.kt +++ b/app/src/main/java/org/mydomain/myscan/MainActivity.kt @@ -2,7 +2,6 @@ package org.mydomain.myscan import android.content.Context import android.content.Intent -import android.graphics.Bitmap import android.media.MediaScannerConnection import android.os.Bundle import android.os.Environment @@ -32,7 +31,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - initOpenCV() + initLibraries() val viewModel: MainViewModel by viewModels { MainViewModel.getFactory(this) } enableEdgeToEdge() setContent { @@ -68,19 +67,15 @@ class MainActivity : ComponentActivity() { viewModel: MainViewModel, context: Context ): () -> Unit = { - val document = viewModel.createPdf() val outputDir = File(cacheDir, "pdfs").apply { mkdirs() } val outputFile = File(outputDir, "scan_${System.currentTimeMillis()}.pdf") var success = true try { - FileOutputStream(outputFile).use { outputStream -> - document.writeTo(outputStream) - } + val fileOutputStream = FileOutputStream(outputFile) + viewModel.createPdf(fileOutputStream) } catch (_: IOException) { Toast.makeText(context, "Failed to share PDF", Toast.LENGTH_SHORT).show() success = false - } finally { - document.close() } if (success) { val uri = FileProvider.getUriForFile( @@ -101,13 +96,12 @@ class MainActivity : ComponentActivity() { viewModel: MainViewModel, context: Context ): () -> Unit = { - val document = viewModel.createPdf() try { val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) if (!downloadsDir.exists()) downloadsDir.mkdirs() val file = File(downloadsDir, "scan_${System.currentTimeMillis()}.pdf") val outputStream = FileOutputStream(file) - document.writeTo(outputStream) + viewModel.createPdf(outputStream) outputStream.flush() outputStream.close() @@ -119,12 +113,12 @@ class MainActivity : ComponentActivity() { } catch (e: Exception) { Log.e("MyScan", "Failed to save PDF", e) 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()) { Log.e("OpenCV", "Initialization failed") } else { diff --git a/app/src/main/java/org/mydomain/myscan/MainViewModel.kt b/app/src/main/java/org/mydomain/myscan/MainViewModel.kt index 04863f9..5a9344b 100644 --- a/app/src/main/java/org/mydomain/myscan/MainViewModel.kt +++ b/app/src/main/java/org/mydomain/myscan/MainViewModel.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream +import java.io.OutputStream class MainViewModel( private val imageSegmentationService: ImageSegmentationService, @@ -128,9 +129,9 @@ class MainViewModel( return BitmapFactory.decodeByteArray(bytes, 0, bytes.size) } - fun createPdf(): PdfDocument { + fun createPdf(outputStream: OutputStream) { val jpegs = imageRepository.imageIds().asSequence() .map { id -> imageRepository.getContent(id) } - return createPdfFromJpegs(jpegs) + writePdfFromJpegs(jpegs, outputStream) } } diff --git a/app/src/main/java/org/mydomain/myscan/PdfGeneration.kt b/app/src/main/java/org/mydomain/myscan/PdfGeneration.kt index e2ac49a..6eb6915 100644 --- a/app/src/main/java/org/mydomain/myscan/PdfGeneration.kt +++ b/app/src/main/java/org/mydomain/myscan/PdfGeneration.kt @@ -1,21 +1,28 @@ package org.mydomain.myscan import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.pdf.PdfDocument 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 -fun createPdfFromJpegs (jpegs: Sequence): PdfDocument { - val document = PdfDocument() - for ((index, jpegBytes) in jpegs.withIndex()) { - val bitmap = BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size) - val pageInfo = PdfDocument.PageInfo.Builder(bitmap.width, bitmap.height, index + 1).create() - val page = document.startPage(pageInfo) - page.canvas.drawBitmap(bitmap, 0f, 0f, null) - document.finishPage(page) +fun writePdfFromJpegs(jpegs: Sequence, outputStream: OutputStream) { + PDDocument().use { document -> + for (jpegBytes in jpegs) { + val image = JPEGFactory.createFromByteArray(document, jpegBytes) + val page = PDPage(PDRectangle(image.width.toFloat(), image.height.toFloat())) + document.addPage(page) + val contentStream = PDPageContentStream(document, page, AppendMode.OVERWRITE, false) + contentStream.drawImage(image, 0f, 0f) + contentStream.close() + } + document.save(outputStream) } - return document } fun resizeImage(original: Bitmap): Bitmap { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 90f7094..bb984f0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ litert = "1.2.0" opencv = "4.11.0" flowlayout = "0.36.0" assertj = "3.27.3" +pdfbox = "2.0.27.0" [libraries] 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-metadata = { group = "com.google.ai.edge.litert", name = "litert-metadata", version.ref = "litert" } 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" }