ImageRepository: make jpegBytes and getThumbnail suspend

This commit is contained in:
Pierre-Yves Nicolas
2026-03-29 10:10:25 +02:00
parent 05a8af577c
commit 7cd3dfd990
6 changed files with 43 additions and 30 deletions

View File

@@ -14,6 +14,7 @@
*/ */
package org.fairscan.app.data package org.fairscan.app.data
import org.fairscan.app.domain.JpegProvider
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.OutputStream import java.io.OutputStream
@@ -25,7 +26,7 @@ data class GeneratedPdf(
) )
fun interface PdfWriter { fun interface PdfWriter {
fun writePdfFromJpegs(jpegs: Sequence<ByteArray>, outputStream: OutputStream): Int suspend fun writePdfFromJpegs(jpegs: List<JpegProvider>, outputStream: OutputStream): Int
} }
class FileManager( class FileManager(
@@ -42,7 +43,7 @@ class FileManager(
} }
} }
fun generatePdf(jpegs: Sequence<ByteArray>): GeneratedPdf { suspend fun generatePdf(jpegs: List<JpegProvider>): GeneratedPdf {
pdfDir.mkdirs() pdfDir.mkdirs()
require(pdfDir.exists() && pdfDir.isDirectory) { "Invalid pdfDir: $pdfDir" } require(pdfDir.exists() && pdfDir.isDirectory) { "Invalid pdfDir: $pdfDir" }
val file = File(pdfDir, "${System.currentTimeMillis()}.pdf") val file = File(pdfDir, "${System.currentTimeMillis()}.pdf")

View File

@@ -170,13 +170,12 @@ class ImageRepository(
saveMetadata() saveMetadata()
} }
fun jpegBytes(key: PageViewKey): ByteArray? = runBlocking(Dispatchers.IO) { suspend fun jpegBytes(key: PageViewKey): ByteArray? =
getOrCompute(imageCache, key, ::computeProcessedImage) getOrCompute(imageCache, key, ::computeProcessedImage)
}
fun getThumbnail(key: PageViewKey): ByteArray? = runBlocking(Dispatchers.IO) {
suspend fun getThumbnail(key: PageViewKey): ByteArray? =
getOrCompute(thumbnailCache, key, ::computeThumbnail) getOrCompute(thumbnailCache, key, ::computeThumbnail)
}
// --- Cache compute functions --- // --- Cache compute functions ---

View File

@@ -22,24 +22,33 @@ import org.fairscan.imageprocessing.resizeForMaxPixels
import org.fairscan.imageprocessing.scaledTo import org.fairscan.imageprocessing.scaledTo
import org.opencv.core.Mat import org.opencv.core.Mat
fun interface JpegProvider {
suspend fun get(): ByteArray
}
suspend fun jpegsForExport( suspend fun jpegsForExport(
imageRepository: ImageRepository, imageRepository: ImageRepository,
exportQuality: ExportQuality exportQuality: ExportQuality
): Sequence<ByteArray> { ): List<JpegProvider> {
val pages = imageRepository.pages().asSequence() val pages = imageRepository.pages()
return when (exportQuality) { return when (exportQuality) {
ExportQuality.BALANCED -> pages.map { jpegBytes(it, imageRepository) } ExportQuality.BALANCED -> pages.map {
JpegProvider { jpegBytes(it, imageRepository) }
}
ExportQuality.LOW -> pages.map { page -> ExportQuality.LOW -> pages.map { page ->
JpegProvider {
resizeJpegBytesForMaxPixels( resizeJpegBytesForMaxPixels(
jpegBytes = jpegBytes(page, imageRepository), jpegBytes = jpegBytes(page, imageRepository),
maxPixels = exportQuality.maxPixels.toDouble(), maxPixels = exportQuality.maxPixels.toDouble(),
jpegQuality = exportQuality.jpegQuality jpegQuality = exportQuality.jpegQuality
) )
} }
}
ExportQuality.HIGH -> pages.map { page -> ExportQuality.HIGH -> pages.map { page ->
JpegProvider {
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
@@ -50,8 +59,9 @@ suspend fun jpegsForExport(
} }
} }
} }
}
private fun jpegBytes(page: ScanPage, imageRepository: ImageRepository): ByteArray { private suspend fun jpegBytes(page: ScanPage, imageRepository: ImageRepository): ByteArray {
val key = page.key() val key = page.key()
return imageRepository.jpegBytes(key) return imageRepository.jpegBytes(key)
?: throw IllegalArgumentException("JPEG not found for $key") ?: throw IllegalArgumentException("JPEG not found for $key")

View File

@@ -22,17 +22,18 @@ import com.tom_roush.pdfbox.pdmodel.common.PDRectangle
import com.tom_roush.pdfbox.pdmodel.graphics.image.JPEGFactory import com.tom_roush.pdfbox.pdmodel.graphics.image.JPEGFactory
import org.fairscan.app.BuildConfig import org.fairscan.app.BuildConfig
import org.fairscan.app.data.PdfWriter import org.fairscan.app.data.PdfWriter
import org.fairscan.app.domain.JpegProvider
import java.io.OutputStream import java.io.OutputStream
import java.util.Calendar import java.util.Calendar
class AndroidPdfWriter : PdfWriter { class AndroidPdfWriter : PdfWriter {
override fun writePdfFromJpegs(jpegs: Sequence<ByteArray>, outputStream: OutputStream): Int { override suspend fun writePdfFromJpegs(jpegs: List<JpegProvider>, outputStream: OutputStream): Int {
val doc = PDDocument() val doc = PDDocument()
doc.documentInformation.creationDate = Calendar.getInstance() doc.documentInformation.creationDate = Calendar.getInstance()
doc.documentInformation.creator = "FairScan ${BuildConfig.VERSION_NAME}" doc.documentInformation.creator = "FairScan ${BuildConfig.VERSION_NAME}"
doc.use { document -> doc.use { document ->
for (jpegBytes in jpegs) { for (jpegBytes in jpegs) {
val image = JPEGFactory.createFromByteArray(document, jpegBytes) val image = JPEGFactory.createFromByteArray(document, jpegBytes.get())
val page = PDPage(PDRectangle(image.width.toFloat(), image.height.toFloat())) val page = PDPage(PDRectangle(image.width.toFloat(), image.height.toFloat()))
document.addPage(page) document.addPage(page)
val contentStream = PDPageContentStream(document, page, AppendMode.OVERWRITE, false) val contentStream = PDPageContentStream(document, page, AppendMode.OVERWRITE, false)

View File

@@ -174,9 +174,9 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
val jpegs = jpegsForExport(imageRepository, exportQuality) val jpegs = jpegsForExport(imageRepository, exportQuality)
val timestamp = System.currentTimeMillis() val timestamp = System.currentTimeMillis()
preparationDir.mkdirs() preparationDir.mkdirs()
val files = jpegs.mapIndexed { index, bytes -> val files = jpegs.mapIndexed { index, jpeg ->
val file = File(preparationDir, "$timestamp-${index + 1}.jpg") val file = File(preparationDir, "$timestamp-${index + 1}.jpg")
file.writeBytes(bytes) file.writeBytes(jpeg.get())
file file
}.toList() }.toList()
val sizeInBytes = files.sumOf { it.length() } val sizeInBytes = files.sumOf { it.length() }

View File

@@ -14,7 +14,9 @@
*/ */
package org.fairscan.app.data package org.fairscan.app.data
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.fairscan.app.domain.JpegProvider
import org.junit.Test import org.junit.Test
import java.io.File import java.io.File
import java.io.OutputStream import java.io.OutputStream
@@ -68,16 +70,16 @@ class FileManagerTest {
} }
@Test @Test
fun generatePdf() { fun generatePdf() = runTest {
val fakePdfWriter = object : PdfWriter { val fakePdfWriter = object : PdfWriter {
override fun writePdfFromJpegs(jpegs: Sequence<ByteArray>, outputStream: OutputStream): Int { override suspend fun writePdfFromJpegs(jpegs: List<JpegProvider>, outputStream: OutputStream): Int {
val list = jpegs.toList() val list = jpegs.toList()
list.forEach { bytes -> outputStream.write(bytes) } list.forEach { bytes -> outputStream.write(bytes.get()) }
return list.size return list.size
} }
} }
val manager = FileManager(pdfDir, externalDir, fakePdfWriter) val manager = FileManager(pdfDir, externalDir, fakePdfWriter)
val jpegs = listOf(byteArrayOf(0x01, 0x02), byteArrayOf(0x11)).asSequence() val jpegs = listOf(byteArrayOf(0x01, 0x02), byteArrayOf(0x11)).map { JpegProvider { it } }
val pdf = manager.generatePdf(jpegs) val pdf = manager.generatePdf(jpegs)
assertThat(pdf.pageCount).isEqualTo(2) assertThat(pdf.pageCount).isEqualTo(2)
assertThat(pdf.sizeInBytes).isEqualTo(3) assertThat(pdf.sizeInBytes).isEqualTo(3)