Avoid swallowing errors during export preparation
This commit is contained in:
@@ -134,8 +134,15 @@ class MainActivity : ComponentActivity() {
|
|||||||
val onExportClick = if (launchMode == LaunchMode.EXTERNAL_SCAN_TO_PDF) {
|
val onExportClick = if (launchMode == LaunchMode.EXTERNAL_SCAN_TO_PDF) {
|
||||||
{
|
{
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val result = exportViewModel.generatePdfForExternalCall()
|
try {
|
||||||
sendActivityResult(result)
|
val result = exportViewModel.generatePdfForExternalCall()
|
||||||
|
sendActivityResult(result)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val message = "Export failed"
|
||||||
|
showToast(message)
|
||||||
|
appContainer.logger.e("MainActivity", message, e)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
viewModel.startNewDocument()
|
viewModel.startNewDocument()
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
@@ -229,6 +236,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showToast(text: String) {
|
||||||
|
Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsScreenWrapper(
|
private fun SettingsScreenWrapper(
|
||||||
settingsViewModel: SettingsViewModel,
|
settingsViewModel: SettingsViewModel,
|
||||||
@@ -246,8 +257,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
settingsViewModel.setExportDirUri(uri.toString())
|
settingsViewModel.setExportDirUri(uri.toString())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.e("Settings", "Failed to set export dir to $uri", e)
|
logger.e("Settings", "Failed to set export dir to $uri", e)
|
||||||
val text = getString(R.string.error_file_picker_result)
|
showToast(this.getString(R.string.error_file_picker_result))
|
||||||
Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,7 +273,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val message = getString(R.string.error_file_picker_launch)
|
val message = getString(R.string.error_file_picker_launch)
|
||||||
logger.e("Settings", message, e)
|
logger.e("Settings", message, e)
|
||||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
showToast(message)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onResetExportDirClick = { settingsViewModel.setExportDirUri(null) },
|
onResetExportDirClick = { settingsViewModel.setExportDirUri(null) },
|
||||||
@@ -288,7 +298,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
clipboard.setClipEntry(
|
clipboard.setClipEntry(
|
||||||
ClipData.newPlainText("FairScan logs", event.logs).toClipEntry()
|
ClipData.newPlainText("FairScan logs", event.logs).toClipEntry()
|
||||||
)
|
)
|
||||||
Toast.makeText(context, msgCopiedLogs, Toast.LENGTH_SHORT).show()
|
showToast(msgCopiedLogs)
|
||||||
}
|
}
|
||||||
is AboutEvent.PrepareEmailWithLastImage -> {
|
is AboutEvent.PrepareEmailWithLastImage -> {
|
||||||
val file = imageRepository.lastAddedSourceFile()
|
val file = imageRepository.lastAddedSourceFile()
|
||||||
@@ -312,8 +322,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
if (isGranted) {
|
if (isGranted) {
|
||||||
exportViewModel.onSaveClicked()
|
exportViewModel.onSaveClicked()
|
||||||
} else {
|
} else {
|
||||||
val message = getString(R.string.storage_permission_denied)
|
showToast(this.getString(R.string.storage_permission_denied))
|
||||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -423,7 +432,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
try {
|
try {
|
||||||
startActivity(chooser)
|
startActivity(chooser)
|
||||||
} catch (_: ActivityNotFoundException) {
|
} catch (_: ActivityNotFoundException) {
|
||||||
Toast.makeText(this, getString(R.string.error_no_app), Toast.LENGTH_SHORT).show()
|
showToast(getString(R.string.error_no_app))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
package org.fairscan.app.domain
|
package org.fairscan.app.domain
|
||||||
|
|
||||||
import org.fairscan.app.data.ImageRepository
|
import org.fairscan.app.data.ImageRepository
|
||||||
|
import org.fairscan.imageprocessing.encodeJpeg
|
||||||
import org.fairscan.imageprocessing.extractDocument
|
import org.fairscan.imageprocessing.extractDocument
|
||||||
import org.fairscan.imageprocessing.resizeForMaxPixels
|
import org.fairscan.imageprocessing.resizeForMaxPixels
|
||||||
import org.fairscan.imageprocessing.scaledTo
|
import org.fairscan.imageprocessing.scaledTo
|
||||||
@@ -29,85 +30,84 @@ suspend fun jpegsForExport(
|
|||||||
|
|
||||||
val pages = imageRepository.pages().asSequence()
|
val pages = imageRepository.pages().asSequence()
|
||||||
return when (exportQuality) {
|
return when (exportQuality) {
|
||||||
ExportQuality.BALANCED -> pages.mapNotNull { imageRepository.jpegBytes(it.key()) }
|
ExportQuality.BALANCED -> pages.map { jpegBytes(it, imageRepository) }
|
||||||
|
|
||||||
ExportQuality.LOW -> pages.mapNotNull { page ->
|
ExportQuality.LOW -> pages.map { page ->
|
||||||
imageRepository.jpegBytes(page.key())?.let { jpeg ->
|
resizeJpegBytesForMaxPixels(
|
||||||
resizeJpegBytesForMaxPixels(
|
jpegBytes = jpegBytes(page, imageRepository),
|
||||||
jpegBytes = jpeg,
|
maxPixels = exportQuality.maxPixels.toDouble(),
|
||||||
maxPixels = exportQuality.maxPixels.toDouble(),
|
jpegQuality = exportQuality.jpegQuality
|
||||||
jpegQuality = exportQuality.jpegQuality
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ExportQuality.HIGH -> pages.mapNotNull { page ->
|
ExportQuality.HIGH -> pages.map { page ->
|
||||||
val sourceJpegBytes = imageRepository.sourceJpegBytes(page.id)
|
val sourceJpegBytes = imageRepository.sourceJpegBytes(page.id)
|
||||||
val pageMetadata = page.metadata
|
val pageMetadata = page.metadata
|
||||||
val manualRotation = page.manualRotation
|
val manualRotation = page.manualRotation
|
||||||
if (sourceJpegBytes != null && pageMetadata != null)
|
if (sourceJpegBytes != null && pageMetadata != null)
|
||||||
prepareJpegForHigh(sourceJpegBytes, pageMetadata, manualRotation, exportQuality)
|
prepareJpegForHigh(sourceJpegBytes, pageMetadata, manualRotation, exportQuality)
|
||||||
else
|
else
|
||||||
imageRepository.jpegBytes(page.key())
|
jpegBytes(page, imageRepository)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resizeJpegBytesForMaxPixels(
|
private fun jpegBytes(page: ScanPage, imageRepository: ImageRepository): ByteArray {
|
||||||
|
val key = page.key()
|
||||||
|
return imageRepository.jpegBytes(key)
|
||||||
|
?: throw IllegalArgumentException("JPEG not found for $key")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resizeJpegBytesForMaxPixels(
|
||||||
jpegBytes: ByteArray,
|
jpegBytes: ByteArray,
|
||||||
maxPixels: Double,
|
maxPixels: Double,
|
||||||
jpegQuality: Int
|
jpegQuality: Int
|
||||||
): ByteArray? {
|
): ByteArray {
|
||||||
val decoded = decodeJpeg(jpegBytes)
|
var decoded: Mat? = null
|
||||||
if (decoded == null)
|
var resized: Mat? = null
|
||||||
return null
|
try {
|
||||||
|
decoded = decodeJpeg(jpegBytes)
|
||||||
val resized = resizeForMaxPixels(decoded, maxPixels)
|
resized = resizeForMaxPixels(decoded, maxPixels)
|
||||||
val outJpegBytes = encodeJpeg(resized, jpegQuality)
|
return encodeJpeg(resized, jpegQuality)
|
||||||
|
} finally {
|
||||||
decoded.release()
|
decoded?.release()
|
||||||
resized.release()
|
resized?.release()
|
||||||
return outJpegBytes
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun prepareJpegForHigh(
|
private fun prepareJpegForHigh(
|
||||||
sourceJpegBytes: ByteArray,
|
sourceJpegBytes: ByteArray,
|
||||||
pageMetadata: PageMetadata,
|
pageMetadata: PageMetadata,
|
||||||
manualRotation: Rotation,
|
manualRotation: Rotation,
|
||||||
exportQuality: ExportQuality,
|
exportQuality: ExportQuality,
|
||||||
): ByteArray? {
|
): ByteArray {
|
||||||
|
|
||||||
val decoded = decodeJpeg(sourceJpegBytes)
|
var decoded: Mat? = null
|
||||||
if (decoded == null)
|
var page: Mat? = null
|
||||||
return null
|
try {
|
||||||
|
decoded = decodeJpeg(sourceJpegBytes)
|
||||||
val quad = pageMetadata.normalizedQuad.scaledTo(1,1,decoded.width(), decoded.height())
|
val quad = pageMetadata.normalizedQuad.scaledTo(1, 1, decoded.width(), decoded.height())
|
||||||
val page = extractDocument(
|
page = extractDocument(
|
||||||
decoded,
|
decoded,
|
||||||
quad,
|
quad,
|
||||||
pageMetadata.baseRotation.add(manualRotation).degrees,
|
pageMetadata.baseRotation.add(manualRotation).degrees,
|
||||||
pageMetadata.isColored,
|
pageMetadata.isColored,
|
||||||
exportQuality.maxPixels)
|
exportQuality.maxPixels
|
||||||
val outJpegBytes = encodeJpeg(page, exportQuality.jpegQuality)
|
)
|
||||||
|
return encodeJpeg(page, exportQuality.jpegQuality)
|
||||||
decoded.release()
|
} finally {
|
||||||
page.release()
|
decoded?.release()
|
||||||
return outJpegBytes
|
page?.release()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decodeJpeg(jpegBytes: ByteArray): Mat? {
|
private fun decodeJpeg(jpegBytes: ByteArray): Mat {
|
||||||
val src = MatOfByte(*jpegBytes)
|
val src = MatOfByte(*jpegBytes)
|
||||||
val decoded = Imgcodecs.imdecode(src, Imgcodecs.IMREAD_COLOR)
|
val decoded = Imgcodecs.imdecode(src, Imgcodecs.IMREAD_COLOR)
|
||||||
src.release()
|
src.release()
|
||||||
if (decoded.empty()) {
|
if (decoded.empty()) {
|
||||||
decoded.release()
|
decoded.release()
|
||||||
return null
|
throw IllegalStateException("Failed to decode JPEG")
|
||||||
}
|
}
|
||||||
return decoded
|
return decoded
|
||||||
}
|
}
|
||||||
|
|
||||||
fun encodeJpeg(mat: Mat, jpegQuality: Int): ByteArray? {
|
|
||||||
return runCatching {
|
|
||||||
org.fairscan.imageprocessing.encodeJpeg(mat, jpegQuality)
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun save(context: Context, saveDir: SaveDir?, exportFormat: ExportFormat) {
|
private suspend fun save(context: Context, saveDir: SaveDir?, exportFormat: ExportFormat) {
|
||||||
val result = applyRenaming() ?: return
|
val result = applyRenaming()
|
||||||
val savedItems = mutableListOf<SavedItem>()
|
val savedItems = mutableListOf<SavedItem>()
|
||||||
val filesForMediaScan = mutableListOf<File>()
|
val filesForMediaScan = mutableListOf<File>()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user