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) {
{
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))
}
}

View File

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

View File

@@ -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<SavedItem>()
val filesForMediaScan = mutableListOf<File>()