Feature: save PDF to device

This commit is contained in:
Pierre-Yves Nicolas
2025-06-01 20:21:34 +02:00
parent ba2a9cf8fd
commit 86f5b27093
4 changed files with 67 additions and 31 deletions

View File

@@ -6,6 +6,8 @@
android:name="android.hardware.camera" android:name="android.hardware.camera"
android:required="false" /> android:required="false" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<!-- Required (on Android 9 and lower) to save files -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application <application
android:allowBackup="true" android:allowBackup="true"

View File

@@ -3,7 +3,9 @@ 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.graphics.Bitmap
import android.media.MediaScannerConnection
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
@@ -23,6 +25,8 @@ import org.mydomain.myscan.view.CameraScreen
import org.mydomain.myscan.view.PagePreviewScreen import org.mydomain.myscan.view.PagePreviewScreen
import org.opencv.android.OpenCVLoader import org.opencv.android.OpenCVLoader
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.io.IOException
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@@ -48,7 +52,8 @@ class MainActivity : ComponentActivity() {
image = screen.image, image = screen.image,
isProcessing = screen.isProcessing, isProcessing = screen.isProcessing,
onBackPressed = { viewModel.navigateTo(Screen.Camera) }, onBackPressed = { viewModel.navigateTo(Screen.Camera) },
onSavePressed = createPdfAndShare(context) onSavePressed = createPdfAndSave(context),
onSharePressed = createPdfAndShare(context),
) )
} }
} }
@@ -61,7 +66,18 @@ class MainActivity : ComponentActivity() {
private fun createPdfAndShare(context: Context): (Bitmap) -> Unit = { bitmap -> private fun createPdfAndShare(context: Context): (Bitmap) -> Unit = { bitmap ->
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")
val success = createPdfFromBitmaps(listOf(bitmap), outputFile) val document = createPdfFromBitmaps(listOf(bitmap))
var success = true
try {
FileOutputStream(outputFile).use { outputStream ->
document.writeTo(outputStream)
}
} catch (_: IOException) {
Toast.makeText(context, "Failed to share PDF", Toast.LENGTH_SHORT).show()
success = false
} finally {
document.close()
}
if (success) { if (success) {
val uri = FileProvider.getUriForFile( val uri = FileProvider.getUriForFile(
context, context,
@@ -74,8 +90,30 @@ class MainActivity : ComponentActivity() {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
} }
startActivity(Intent.createChooser(shareIntent, "Share PDF")) startActivity(Intent.createChooser(shareIntent, "Share PDF"))
} else { }
}
fun createPdfAndSave(context: Context): (Bitmap) -> Unit = { bitmap ->
val document = createPdfFromBitmaps(listOf(bitmap))
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)
outputStream.flush()
outputStream.close()
MediaScannerConnection.scanFile(
context, arrayOf(file.absolutePath), arrayOf("application/pdf"), null
)
Toast.makeText(context, "Saved PDF in Downloads", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
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()
} }
} }

View File

@@ -3,37 +3,24 @@ package org.mydomain.myscan
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.pdf.PdfDocument import android.graphics.pdf.PdfDocument
import android.util.Log
import androidx.core.graphics.scale import androidx.core.graphics.scale
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import kotlin.math.max import kotlin.math.max
fun createPdfFromBitmaps(bitmaps: List<Bitmap>, outputFile: File): Boolean { fun createPdfFromBitmaps (bitmaps: List<Bitmap>): PdfDocument {
val document = PdfDocument() val document = PdfDocument()
try {
for ((index, bitmap) in bitmaps.map { resizeImage(it) }.withIndex()) { for ((index, bitmap) in bitmaps.map { resizeImage(it) }.withIndex()) {
val jpegStream = ByteArrayOutputStream() val jpegStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 72, jpegStream) bitmap.compress(Bitmap.CompressFormat.JPEG, 72, jpegStream)
val compressedBytes = jpegStream.toByteArray() val compressedBytes = jpegStream.toByteArray()
val compressedBitmap = BitmapFactory.decodeByteArray(compressedBytes, 0, compressedBytes.size) val compressedBitmap =
BitmapFactory.decodeByteArray(compressedBytes, 0, compressedBytes.size)
val pageInfo = PdfDocument.PageInfo.Builder(bitmap.width, bitmap.height, index + 1).create() val pageInfo = PdfDocument.PageInfo.Builder(bitmap.width, bitmap.height, index + 1).create()
val page = document.startPage(pageInfo) val page = document.startPage(pageInfo)
page.canvas.drawBitmap(compressedBitmap, 0f, 0f, null) page.canvas.drawBitmap(compressedBitmap, 0f, 0f, null)
document.finishPage(page) document.finishPage(page)
} }
FileOutputStream(outputFile).use { outputStream -> return document
document.writeTo(outputStream)
}
return true
} catch (e: IOException) {
Log.e("MyScan", "Error writing PDF: ${e.message}")
return false
} finally {
document.close()
}
} }
fun resizeImage(original: Bitmap): Bitmap { fun resizeImage(original: Bitmap): Bitmap {

View File

@@ -3,8 +3,11 @@ package org.mydomain.myscan.view
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button import androidx.compose.material3.Button
@@ -25,6 +28,7 @@ fun PagePreviewScreen(
isProcessing: Boolean, isProcessing: Boolean,
onBackPressed: () -> Unit, onBackPressed: () -> Unit,
onSavePressed: (Bitmap) -> Unit, onSavePressed: (Bitmap) -> Unit,
onSharePressed: (Bitmap) -> Unit,
) { ) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
when { when {
@@ -41,13 +45,18 @@ fun PagePreviewScreen(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Fit contentScale = ContentScale.Fit
) )
Button ( Row(
onClick = { onSavePressed(image) },
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
.padding(16.dp) .padding(16.dp)
) { ) {
Text("Save as PDF") Button (onClick = { onSavePressed(image) }) {
Text("Save PDF")
}
Spacer(modifier = Modifier.width(8.dp))
Button (onClick = { onSharePressed(image) }) {
Text("Share PDF")
}
} }
} }
else -> { else -> {