From f130d33eba5deee3be542feceec6b0a8f645a920 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Nicolas <6371790+pynicolas@users.noreply.github.com> Date: Wed, 25 Mar 2026 18:47:45 +0100 Subject: [PATCH] Avoid swallowing errors during export preparation --- .../java/org/fairscan/app/MainActivity.kt | 27 +++-- .../fairscan/app/domain/ExportPreparation.kt | 98 +++++++++---------- .../app/ui/screens/export/ExportViewModel.kt | 2 +- 3 files changed, 68 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/org/fairscan/app/MainActivity.kt b/app/src/main/java/org/fairscan/app/MainActivity.kt index 3728cee..d1f87b1 100644 --- a/app/src/main/java/org/fairscan/app/MainActivity.kt +++ b/app/src/main/java/org/fairscan/app/MainActivity.kt @@ -134,8 +134,15 @@ class MainActivity : ComponentActivity() { val onExportClick = if (launchMode == LaunchMode.EXTERNAL_SCAN_TO_PDF) { { lifecycleScope.launch { - val result = exportViewModel.generatePdfForExternalCall() - sendActivityResult(result) + try { + 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() finish() } @@ -229,6 +236,10 @@ class MainActivity : ComponentActivity() { } } + private fun showToast(text: String) { + Toast.makeText(this, text, Toast.LENGTH_SHORT).show() + } + @Composable private fun SettingsScreenWrapper( settingsViewModel: SettingsViewModel, @@ -246,8 +257,7 @@ class MainActivity : ComponentActivity() { settingsViewModel.setExportDirUri(uri.toString()) } catch (e: Exception) { logger.e("Settings", "Failed to set export dir to $uri", e) - val text = getString(R.string.error_file_picker_result) - Toast.makeText(this, text, Toast.LENGTH_SHORT).show() + showToast(this.getString(R.string.error_file_picker_result)) } } } @@ -263,7 +273,7 @@ class MainActivity : ComponentActivity() { } catch (e: Exception) { val message = getString(R.string.error_file_picker_launch) logger.e("Settings", message, e) - Toast.makeText(this, message, Toast.LENGTH_SHORT).show() + showToast(message) } }, onResetExportDirClick = { settingsViewModel.setExportDirUri(null) }, @@ -288,7 +298,7 @@ class MainActivity : ComponentActivity() { clipboard.setClipEntry( ClipData.newPlainText("FairScan logs", event.logs).toClipEntry() ) - Toast.makeText(context, msgCopiedLogs, Toast.LENGTH_SHORT).show() + showToast(msgCopiedLogs) } is AboutEvent.PrepareEmailWithLastImage -> { val file = imageRepository.lastAddedSourceFile() @@ -312,8 +322,7 @@ class MainActivity : ComponentActivity() { if (isGranted) { exportViewModel.onSaveClicked() } else { - val message = getString(R.string.storage_permission_denied) - Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + showToast(this.getString(R.string.storage_permission_denied)) } } LaunchedEffect(Unit) { @@ -423,7 +432,7 @@ class MainActivity : ComponentActivity() { try { startActivity(chooser) } catch (_: ActivityNotFoundException) { - Toast.makeText(this, getString(R.string.error_no_app), Toast.LENGTH_SHORT).show() + showToast(getString(R.string.error_no_app)) } } diff --git a/app/src/main/java/org/fairscan/app/domain/ExportPreparation.kt b/app/src/main/java/org/fairscan/app/domain/ExportPreparation.kt index eaf4273..ab4d534 100644 --- a/app/src/main/java/org/fairscan/app/domain/ExportPreparation.kt +++ b/app/src/main/java/org/fairscan/app/domain/ExportPreparation.kt @@ -15,6 +15,7 @@ package org.fairscan.app.domain import org.fairscan.app.data.ImageRepository +import org.fairscan.imageprocessing.encodeJpeg import org.fairscan.imageprocessing.extractDocument import org.fairscan.imageprocessing.resizeForMaxPixels import org.fairscan.imageprocessing.scaledTo @@ -29,85 +30,84 @@ suspend fun jpegsForExport( val pages = imageRepository.pages().asSequence() return when (exportQuality) { - ExportQuality.BALANCED -> pages.mapNotNull { imageRepository.jpegBytes(it.key()) } + ExportQuality.BALANCED -> pages.map { jpegBytes(it, imageRepository) } - ExportQuality.LOW -> pages.mapNotNull { page -> - imageRepository.jpegBytes(page.key())?.let { jpeg -> - resizeJpegBytesForMaxPixels( - jpegBytes = jpeg, - maxPixels = exportQuality.maxPixels.toDouble(), - jpegQuality = exportQuality.jpegQuality - ) - } + ExportQuality.LOW -> pages.map { page -> + resizeJpegBytesForMaxPixels( + jpegBytes = jpegBytes(page, imageRepository), + maxPixels = exportQuality.maxPixels.toDouble(), + jpegQuality = exportQuality.jpegQuality + ) } - ExportQuality.HIGH -> pages.mapNotNull { page -> + ExportQuality.HIGH -> pages.map { page -> val sourceJpegBytes = imageRepository.sourceJpegBytes(page.id) val pageMetadata = page.metadata val manualRotation = page.manualRotation if (sourceJpegBytes != null && pageMetadata != null) prepareJpegForHigh(sourceJpegBytes, pageMetadata, manualRotation, exportQuality) 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, maxPixels: Double, jpegQuality: Int -): ByteArray? { - val decoded = decodeJpeg(jpegBytes) - if (decoded == null) - return null - - val resized = resizeForMaxPixels(decoded, maxPixels) - val outJpegBytes = encodeJpeg(resized, jpegQuality) - - decoded.release() - resized.release() - return outJpegBytes +): ByteArray { + var decoded: Mat? = null + var resized: Mat? = null + try { + decoded = decodeJpeg(jpegBytes) + resized = resizeForMaxPixels(decoded, maxPixels) + return encodeJpeg(resized, jpegQuality) + } finally { + decoded?.release() + resized?.release() + } } -fun prepareJpegForHigh( +private fun prepareJpegForHigh( sourceJpegBytes: ByteArray, pageMetadata: PageMetadata, manualRotation: Rotation, exportQuality: ExportQuality, -): ByteArray? { +): ByteArray { - val decoded = decodeJpeg(sourceJpegBytes) - if (decoded == null) - return null - - val quad = pageMetadata.normalizedQuad.scaledTo(1,1,decoded.width(), decoded.height()) - val page = extractDocument( - decoded, - quad, - pageMetadata.baseRotation.add(manualRotation).degrees, - pageMetadata.isColored, - exportQuality.maxPixels) - val outJpegBytes = encodeJpeg(page, exportQuality.jpegQuality) - - decoded.release() - page.release() - return outJpegBytes + var decoded: Mat? = null + var page: Mat? = null + try { + decoded = decodeJpeg(sourceJpegBytes) + val quad = pageMetadata.normalizedQuad.scaledTo(1, 1, decoded.width(), decoded.height()) + page = extractDocument( + decoded, + quad, + pageMetadata.baseRotation.add(manualRotation).degrees, + pageMetadata.isColored, + exportQuality.maxPixels + ) + return encodeJpeg(page, exportQuality.jpegQuality) + } finally { + decoded?.release() + page?.release() + } } -fun decodeJpeg(jpegBytes: ByteArray): Mat? { +private fun decodeJpeg(jpegBytes: ByteArray): Mat { val src = MatOfByte(*jpegBytes) val decoded = Imgcodecs.imdecode(src, Imgcodecs.IMREAD_COLOR) src.release() if (decoded.empty()) { decoded.release() - return null + throw IllegalStateException("Failed to decode JPEG") } return decoded } - -fun encodeJpeg(mat: Mat, jpegQuality: Int): ByteArray? { - return runCatching { - org.fairscan.imageprocessing.encodeJpeg(mat, jpegQuality) - }.getOrNull() -} diff --git a/app/src/main/java/org/fairscan/app/ui/screens/export/ExportViewModel.kt b/app/src/main/java/org/fairscan/app/ui/screens/export/ExportViewModel.kt index b9d85ba..381c7a4 100644 --- a/app/src/main/java/org/fairscan/app/ui/screens/export/ExportViewModel.kt +++ b/app/src/main/java/org/fairscan/app/ui/screens/export/ExportViewModel.kt @@ -276,7 +276,7 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit } private suspend fun save(context: Context, saveDir: SaveDir?, exportFormat: ExportFormat) { - val result = applyRenaming() ?: return + val result = applyRenaming() val savedItems = mutableListOf() val filesForMediaScan = mutableListOf()