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:required="false" />
<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
android:allowBackup="true"

View File

@@ -3,7 +3,9 @@ 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
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
@@ -23,6 +25,8 @@ import org.mydomain.myscan.view.CameraScreen
import org.mydomain.myscan.view.PagePreviewScreen
import org.opencv.android.OpenCVLoader
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
class MainActivity : ComponentActivity() {
@@ -48,7 +52,8 @@ class MainActivity : ComponentActivity() {
image = screen.image,
isProcessing = screen.isProcessing,
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 ->
val outputDir = File(cacheDir, "pdfs").apply { mkdirs() }
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) {
val uri = FileProvider.getUriForFile(
context,
@@ -74,8 +90,30 @@ class MainActivity : ComponentActivity() {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
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()
} finally {
document.close()
}
}

View File

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

View File

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