Feature: save PDF to device
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 -> {
|
||||||
|
|||||||
Reference in New Issue
Block a user