Avoid swallowing errors during export preparation

This commit is contained in:
Pierre-Yves Nicolas
2026-03-25 18:47:45 +01:00
parent 92914c1730
commit f130d33eba
3 changed files with 68 additions and 59 deletions

View File

@@ -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 {
try {
val result = exportViewModel.generatePdfForExternalCall() val result = exportViewModel.generatePdfForExternalCall()
sendActivityResult(result) 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))
} }
} }

View File

@@ -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 = jpeg, jpegBytes = jpegBytes(page, imageRepository),
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()
}

View File

@@ -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>()