From 3681d5771db75fc73d6368d1828f05ae189640bd Mon Sep 17 00:00:00 2001 From: Pierre-Yves Nicolas <6371790+pynicolas@users.noreply.github.com> Date: Sun, 29 Mar 2026 11:22:08 +0200 Subject: [PATCH] Introduce Jpeg class --- .../app/domain/DocumentDetectionTest.kt | 4 +- .../java/org/fairscan/app/MainViewModel.kt | 3 -- .../org/fairscan/app/data/ImageRepository.kt | 41 ++++++++++--------- .../fairscan/app/data/ImageTransformations.kt | 6 ++- .../org/fairscan/app/domain/CapturedPage.kt | 4 +- .../fairscan/app/domain/ExportPreparation.kt | 34 ++++++++------- .../main/java/org/fairscan/app/domain/Jpeg.kt | 29 +++++++++++++ .../fairscan/app/platform/AndroidPdfWriter.kt | 2 +- .../platform/OpenCvImageTransformations.kt | 17 ++++---- .../app/ui/screens/camera/CameraScreen.kt | 15 ++++--- .../app/ui/screens/camera/CameraViewModel.kt | 8 ++-- .../app/ui/screens/export/ExportViewModel.kt | 2 +- .../org/fairscan/app/data/FileManagerTest.kt | 5 ++- .../fairscan/app/data/ImageRepositoryTest.kt | 39 ++++++++++-------- 14 files changed, 119 insertions(+), 90 deletions(-) create mode 100644 app/src/main/java/org/fairscan/app/domain/Jpeg.kt diff --git a/app/src/androidTest/java/org/fairscan/app/domain/DocumentDetectionTest.kt b/app/src/androidTest/java/org/fairscan/app/domain/DocumentDetectionTest.kt index 39d7052..9a8a71b 100644 --- a/app/src/androidTest/java/org/fairscan/app/domain/DocumentDetectionTest.kt +++ b/app/src/androidTest/java/org/fairscan/app/domain/DocumentDetectionTest.kt @@ -51,7 +51,7 @@ class DocumentDetectionTest { listOf("img01.jpg", "img02.jpg", "img03.jpg").forEach { imageFileName -> val inputStream = context.assets.open("uncropped/$imageFileName") val bitmap = BitmapFactory.decodeStream(inputStream) - var outputJpeg: ByteArray? = null + var outputJpeg: Jpeg? = null val segmentationResult = runBlocking { segmentationService.runSegmentationAndReturn(bitmap) @@ -64,7 +64,7 @@ class DocumentDetectionTest { quad.scaledTo(mask.width, mask.height, bitmap.width, bitmap.height) outputJpeg = extractDocumentFromBitmap(bitmap, resizedQuad, 0, mask, scope).pageJpeg val file = File(context.getExternalFilesDir(null), imageFileName) - file.writeBytes(outputJpeg) + file.writeBytes(outputJpeg.bytes) Log.i("DocumentDetectionTest", "Image saved to ${file.absolutePath}") } } diff --git a/app/src/main/java/org/fairscan/app/MainViewModel.kt b/app/src/main/java/org/fairscan/app/MainViewModel.kt index 8178717..c7361da 100644 --- a/app/src/main/java/org/fairscan/app/MainViewModel.kt +++ b/app/src/main/java/org/fairscan/app/MainViewModel.kt @@ -145,9 +145,6 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode } } - private fun ByteArray.toBitmap() : Bitmap = - BitmapFactory.decodeByteArray(this, 0, this.size) - fun handleImageCaptured(capturedPage: CapturedPage) { viewModelScope.launch { val pages = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/org/fairscan/app/data/ImageRepository.kt b/app/src/main/java/org/fairscan/app/data/ImageRepository.kt index e3f7286..4332877 100644 --- a/app/src/main/java/org/fairscan/app/data/ImageRepository.kt +++ b/app/src/main/java/org/fairscan/app/data/ImageRepository.kt @@ -19,7 +19,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext @@ -29,6 +28,7 @@ import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import org.fairscan.app.domain.ExportQuality +import org.fairscan.app.domain.Jpeg import org.fairscan.app.domain.PageMetadata import org.fairscan.app.domain.PageViewKey import org.fairscan.app.domain.Rotation @@ -64,8 +64,8 @@ class ImageRepository( private val json = Json { prettyPrint = false; encodeDefaults = true } private var pages: PageStore = PageStore(loadPages()) - private val imageCache = createLruCache>(maxEntries = 50) - private val thumbnailCache = createLruCache>(maxEntries = 200) + private val imageCache = createLruCache>(maxEntries = 50) + private val thumbnailCache = createLruCache>(maxEntries = 200) private fun createLruCache(maxEntries: Int): MutableMap = Collections.synchronizedMap(object : LinkedHashMap(16, 0.75f, true) { @@ -139,12 +139,12 @@ class ImageRepository( } } - suspend fun add(pageBytes: ByteArray, sourceBytes: ByteArray, metadata: PageMetadata) = + suspend fun add(processed: Jpeg, source: Jpeg, metadata: PageMetadata) = mutex.withLock { val id = "${System.currentTimeMillis()}" val fileName = "$id.jpg" - File(processedDir, fileName).writeBytes(pageBytes) - File(sourceDir, fileName).writeBytes(sourceBytes) + File(processedDir, fileName).writeBytes(processed.bytes) + File(sourceDir, fileName).writeBytes(source.bytes) pages.addOrReplace( PageV2( id = id, @@ -157,7 +157,7 @@ class ImageRepository( saveMetadata() // Pre-populate cache for R0 val key = PageViewKey(id, Rotation.R0) - imageCache.put(key, CompletableDeferred(pageBytes)) + imageCache.put(key, CompletableDeferred(processed)) } suspend fun rotate(id: String, clockwise: Boolean) = mutex.withLock { @@ -170,20 +170,20 @@ class ImageRepository( saveMetadata() } - suspend fun jpegBytes(key: PageViewKey): ByteArray? = + suspend fun jpegBytes(key: PageViewKey): Jpeg? = getOrCompute(imageCache, key, ::computeProcessedImage) - suspend fun getThumbnail(key: PageViewKey): ByteArray? = + suspend fun getThumbnail(key: PageViewKey): Jpeg? = getOrCompute(thumbnailCache, key, ::computeThumbnail) // --- Cache compute functions --- private suspend fun getOrCompute( - cache: MutableMap>, + cache: MutableMap>, key: PageViewKey, - compute: suspend (PageViewKey) -> ByteArray? - ): ByteArray? { + compute: suspend (PageViewKey) -> Jpeg? + ): Jpeg? { val deferred = cache.computeIfAbsent(key) { k -> scope.async(Dispatchers.IO) { compute(k) } } @@ -195,32 +195,33 @@ class ImageRepository( } } - private suspend fun computeProcessedImage(key: PageViewKey): ByteArray? = + private suspend fun computeProcessedImage(key: PageViewKey): Jpeg? = withContext(Dispatchers.IO) { val baseFile = File(processedDir, "${key.pageId}.jpg") if (!baseFile.exists()) return@withContext null + val baseJpeg = Jpeg(baseFile.readBytes()) if (key.rotation == Rotation.R0) { - baseFile.readBytes() + baseJpeg } else { transformations.rotate( - baseFile.readBytes(), + baseJpeg, key.rotation.degrees, ExportQuality.BALANCED.jpegQuality) } } - private suspend fun computeThumbnail(key: PageViewKey): ByteArray? = + private suspend fun computeThumbnail(key: PageViewKey): Jpeg? = withContext(Dispatchers.IO) { - val imageBytes = getOrCompute(imageCache, key, ::computeProcessedImage) + val processed = getOrCompute(imageCache, key, ::computeProcessedImage) ?: return@withContext null - transformations.resize(imageBytes, thumbnailSizePx) + transformations.resize(processed, thumbnailSizePx) } // --- Other operations --- - fun sourceJpegBytes(id: String): ByteArray? { + fun source(id: String): Jpeg? { val file = File(sourceDir, "$id.jpg") - return if (file.exists()) file.readBytes() else null + return if (file.exists()) Jpeg(file.readBytes()) else null } suspend fun movePage(id: String, newIndex: Int) = mutex.withLock { diff --git a/app/src/main/java/org/fairscan/app/data/ImageTransformations.kt b/app/src/main/java/org/fairscan/app/data/ImageTransformations.kt index 3b0eb29..bff485a 100644 --- a/app/src/main/java/org/fairscan/app/data/ImageTransformations.kt +++ b/app/src/main/java/org/fairscan/app/data/ImageTransformations.kt @@ -14,10 +14,12 @@ */ package org.fairscan.app.data +import org.fairscan.app.domain.Jpeg + interface ImageTransformations { - fun rotate(input: ByteArray, rotationDegrees: Int, jpegQuality: Int): ByteArray + fun rotate(input: Jpeg, rotationDegrees: Int, jpegQuality: Int): Jpeg - fun resize(input: ByteArray, maxSize: Int): ByteArray + fun resize(input: Jpeg, maxSize: Int): Jpeg } \ No newline at end of file diff --git a/app/src/main/java/org/fairscan/app/domain/CapturedPage.kt b/app/src/main/java/org/fairscan/app/domain/CapturedPage.kt index 4734db8..c58f461 100644 --- a/app/src/main/java/org/fairscan/app/domain/CapturedPage.kt +++ b/app/src/main/java/org/fairscan/app/domain/CapturedPage.kt @@ -18,7 +18,7 @@ package org.fairscan.app.domain import kotlinx.coroutines.Deferred data class CapturedPage( - val pageJpeg: ByteArray, - val sourceJpeg: Deferred, + val pageJpeg: Jpeg, + val sourceJpeg: Deferred, val metadata: PageMetadata, ) 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 8b6cdd8..ea92511 100644 --- a/app/src/main/java/org/fairscan/app/domain/ExportPreparation.kt +++ b/app/src/main/java/org/fairscan/app/domain/ExportPreparation.kt @@ -15,15 +15,13 @@ package org.fairscan.app.domain import org.fairscan.app.data.ImageRepository -import org.fairscan.imageprocessing.decodeJpeg -import org.fairscan.imageprocessing.encodeJpeg import org.fairscan.imageprocessing.extractDocument import org.fairscan.imageprocessing.resizeForMaxPixels import org.fairscan.imageprocessing.scaledTo import org.opencv.core.Mat fun interface JpegProvider { - suspend fun get(): ByteArray + suspend fun get(): Jpeg } suspend fun jpegsForExport( @@ -34,13 +32,13 @@ suspend fun jpegsForExport( val pages = imageRepository.pages() return when (exportQuality) { ExportQuality.BALANCED -> pages.map { - JpegProvider { jpegBytes(it, imageRepository) } + JpegProvider { jpeg(it, imageRepository) } } ExportQuality.LOW -> pages.map { page -> JpegProvider { resizeJpegBytesForMaxPixels( - jpegBytes = jpegBytes(page, imageRepository), + jpeg = jpeg(page, imageRepository), maxPixels = exportQuality.maxPixels.toDouble(), jpegQuality = exportQuality.jpegQuality ) @@ -49,35 +47,35 @@ suspend fun jpegsForExport( ExportQuality.HIGH -> pages.map { page -> JpegProvider { - val sourceJpegBytes = imageRepository.sourceJpegBytes(page.id) + val source = imageRepository.source(page.id) val pageMetadata = page.metadata val manualRotation = page.manualRotation - if (sourceJpegBytes != null && pageMetadata != null) - prepareJpegForHigh(sourceJpegBytes, pageMetadata, manualRotation, exportQuality) + if (source != null && pageMetadata != null) + prepareJpegForHigh(source, pageMetadata, manualRotation, exportQuality) else - jpegBytes(page, imageRepository) + jpeg(page, imageRepository) } } } } -private suspend fun jpegBytes(page: ScanPage, imageRepository: ImageRepository): ByteArray { +private suspend fun jpeg(page: ScanPage, imageRepository: ImageRepository): Jpeg { val key = page.key() return imageRepository.jpegBytes(key) ?: throw IllegalArgumentException("JPEG not found for $key") } private fun resizeJpegBytesForMaxPixels( - jpegBytes: ByteArray, + jpeg: Jpeg, maxPixels: Double, jpegQuality: Int -): ByteArray { +): Jpeg { var decoded: Mat? = null var resized: Mat? = null try { - decoded = decodeJpeg(jpegBytes) + decoded = jpeg.toMat() resized = resizeForMaxPixels(decoded, maxPixels) - return encodeJpeg(resized, jpegQuality) + return Jpeg.fromMat(resized, jpegQuality) } finally { decoded?.release() resized?.release() @@ -85,16 +83,16 @@ private fun resizeJpegBytesForMaxPixels( } private fun prepareJpegForHigh( - sourceJpegBytes: ByteArray, + source: Jpeg, pageMetadata: PageMetadata, manualRotation: Rotation, exportQuality: ExportQuality, -): ByteArray { +): Jpeg { var decoded: Mat? = null var page: Mat? = null try { - decoded = decodeJpeg(sourceJpegBytes) + decoded = source.toMat() val quad = pageMetadata.normalizedQuad.scaledTo(1, 1, decoded.width(), decoded.height()) page = extractDocument( decoded, @@ -103,7 +101,7 @@ private fun prepareJpegForHigh( pageMetadata.isColored, exportQuality.maxPixels ) - return encodeJpeg(page, exportQuality.jpegQuality) + return Jpeg.fromMat(page, exportQuality.jpegQuality) } finally { decoded?.release() page?.release() diff --git a/app/src/main/java/org/fairscan/app/domain/Jpeg.kt b/app/src/main/java/org/fairscan/app/domain/Jpeg.kt new file mode 100644 index 0000000..edb388e --- /dev/null +++ b/app/src/main/java/org/fairscan/app/domain/Jpeg.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2025-2026 Pierre-Yves Nicolas + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package org.fairscan.app.domain + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import org.fairscan.imageprocessing.decodeJpeg +import org.fairscan.imageprocessing.encodeJpeg +import org.opencv.core.Mat + +class Jpeg(val bytes: ByteArray) { + companion object { + fun fromMat(mat: Mat, jpegQuality: Int): Jpeg = Jpeg(encodeJpeg(mat, jpegQuality)) + } + fun toBitmap() : Bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + fun toMat() : Mat = decodeJpeg(bytes) +} diff --git a/app/src/main/java/org/fairscan/app/platform/AndroidPdfWriter.kt b/app/src/main/java/org/fairscan/app/platform/AndroidPdfWriter.kt index 3fff6e4..47ab079 100644 --- a/app/src/main/java/org/fairscan/app/platform/AndroidPdfWriter.kt +++ b/app/src/main/java/org/fairscan/app/platform/AndroidPdfWriter.kt @@ -33,7 +33,7 @@ class AndroidPdfWriter : PdfWriter { doc.documentInformation.creator = "FairScan ${BuildConfig.VERSION_NAME}" doc.use { document -> for (jpegBytes in jpegs) { - val image = JPEGFactory.createFromByteArray(document, jpegBytes.get()) + val image = JPEGFactory.createFromByteArray(document, jpegBytes.get().bytes) val page = PDPage(PDRectangle(image.width.toFloat(), image.height.toFloat())) document.addPage(page) val contentStream = PDPageContentStream(document, page, AppendMode.OVERWRITE, false) diff --git a/app/src/main/java/org/fairscan/app/platform/OpenCvImageTransformations.kt b/app/src/main/java/org/fairscan/app/platform/OpenCvImageTransformations.kt index 2adfd23..a456fc7 100644 --- a/app/src/main/java/org/fairscan/app/platform/OpenCvImageTransformations.kt +++ b/app/src/main/java/org/fairscan/app/platform/OpenCvImageTransformations.kt @@ -15,8 +15,7 @@ package org.fairscan.app.platform import org.fairscan.app.data.ImageTransformations -import org.fairscan.imageprocessing.decodeJpeg -import org.fairscan.imageprocessing.encodeJpeg +import org.fairscan.app.domain.Jpeg import org.opencv.core.Mat import org.opencv.core.Size import org.opencv.imgproc.Imgproc @@ -25,16 +24,16 @@ import kotlin.math.min class OpenCvTransformations : ImageTransformations { override fun rotate( - input: ByteArray, + input: Jpeg, rotationDegrees: Int, jpegQuality: Int - ): ByteArray { + ): Jpeg { return transform(input, jpegQuality) { org.fairscan.imageprocessing.rotate(it, rotationDegrees) } } - override fun resize(input: ByteArray, maxSize: Int): ByteArray { + override fun resize(input: Jpeg, maxSize: Int): Jpeg { return transform(input, 85) { src -> val ratio = min(maxSize.toFloat() / src.width(), maxSize.toFloat() / src.height()) val newW = (src.width() * ratio).toDouble() @@ -46,15 +45,15 @@ class OpenCvTransformations : ImageTransformations { } private fun transform( - inBytes: ByteArray, + inJpeg: Jpeg, jpegQuality: Int, transform: (Mat) -> Mat, - ): ByteArray { - val input = decodeJpeg(inBytes) + ): Jpeg { + val input = inJpeg.toMat() var output: Mat? = null try { output = transform.invoke(input) - return encodeJpeg(output, jpegQuality) + return Jpeg.fromMat(output, jpegQuality) } finally { input.release() output?.release() diff --git a/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraScreen.kt b/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraScreen.kt index 7b13f03..49b72b4 100644 --- a/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraScreen.kt +++ b/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraScreen.kt @@ -91,6 +91,7 @@ import kotlinx.coroutines.delay import org.fairscan.app.MainViewModel import org.fairscan.app.R import org.fairscan.app.domain.CapturedPage +import org.fairscan.app.domain.Jpeg import org.fairscan.app.domain.PageMetadata import org.fairscan.app.domain.Rotation.R0 import org.fairscan.app.ui.Navigation @@ -286,7 +287,7 @@ private fun CameraScreenScaffold( ) } if (cameraUiState.captureState is CaptureState.CapturePreview) { - val page = bitmap(cameraUiState.captureState.capturedPage.pageJpeg) + val page = cameraUiState.captureState.capturedPage.pageJpeg.toBitmap() CapturedImage(page.asImageBitmap(), thumbnailCoords) } } @@ -534,10 +535,10 @@ fun CameraScreenPreviewWithProcessedImage() { val p = Point(0 , 0) val quad = Quad(p, p, p, p) ScreenPreview(CaptureState.CapturePreview( - bitmap(debugImage("uncropped/img01.jpg")), + debugImage("uncropped/img01.jpg").toBitmap(), CapturedPage( debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg"), - CompletableDeferred(ByteArray(0)), + CompletableDeferred(Jpeg(ByteArray(0))), PageMetadata(quad, R0, false)))) } @@ -560,7 +561,7 @@ private fun ScreenPreview(captureState: CaptureState, rotationDegrees: Float = 0 contentAlignment = Alignment.TopCenter ) { Image( - bitmap(debugImage("uncropped/img01.jpg")).asImageBitmap(), + debugImage("uncropped/img01.jpg").toBitmap().asImageBitmap(), modifier=Modifier.rotate(rotationDegrees), contentDescription = null ) @@ -592,9 +593,7 @@ private fun ScreenPreview(captureState: CaptureState, rotationDegrees: Float = 0 } @Composable -private fun debugImage(imgName: String): ByteArray { +private fun debugImage(imgName: String): Jpeg { val context = LocalContext.current - return context.assets.open(imgName).readBytes() + return Jpeg(context.assets.open(imgName).readBytes()) } - -private fun bitmap(jpeg: ByteArray): Bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.size) diff --git a/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraViewModel.kt b/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraViewModel.kt index 43fbc29..fc0d6c3 100644 --- a/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraViewModel.kt +++ b/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraViewModel.kt @@ -32,13 +32,13 @@ import kotlinx.coroutines.withContext import org.fairscan.app.AppContainer import org.fairscan.app.domain.CapturedPage import org.fairscan.app.domain.ExportQuality +import org.fairscan.app.domain.Jpeg import org.fairscan.app.domain.PageMetadata import org.fairscan.app.domain.Rotation import org.fairscan.imageprocessing.ImageSize import org.fairscan.imageprocessing.Mask import org.fairscan.imageprocessing.Quad import org.fairscan.imageprocessing.detectDocumentQuad -import org.fairscan.imageprocessing.encodeJpeg import org.fairscan.imageprocessing.extractDocument import org.fairscan.imageprocessing.isColoredDocument import org.fairscan.imageprocessing.scaledTo @@ -197,14 +197,14 @@ class CameraViewModel(appContainer: AppContainer): ViewModel() { } } -private fun compressJpeg(bitmap: Bitmap, quality: Int): ByteArray { +private fun compressJpeg(bitmap: Bitmap, quality: Int): Jpeg { val rgba = Mat() Utils.bitmapToMat(bitmap, rgba) val bgr = Mat() Imgproc.cvtColor(rgba, bgr, Imgproc.COLOR_RGBA2BGR) rgba.release() return try { - encodeJpeg(bgr, quality) + Jpeg.fromMat(bgr, quality) } finally { bgr.release() } @@ -233,7 +233,7 @@ fun extractDocumentFromBitmap( val isColored = isColoredDocument(bgr, mask, quad) val maxPixels = ExportQuality.BALANCED.maxPixels val page = extractDocument(bgr, quad, rotationDegrees, isColored, maxPixels) - val pageJpeg = encodeJpeg(page, ExportQuality.BALANCED.jpegQuality) + val pageJpeg = Jpeg.fromMat(page, ExportQuality.BALANCED.jpegQuality) bgr.release() page.release() 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 c48adde..d8e49d0 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 @@ -176,7 +176,7 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit preparationDir.mkdirs() val files = jpegs.mapIndexed { index, jpeg -> val file = File(preparationDir, "$timestamp-${index + 1}.jpg") - file.writeBytes(jpeg.get()) + file.writeBytes(jpeg.get().bytes) file }.toList() val sizeInBytes = files.sumOf { it.length() } diff --git a/app/src/test/java/org/fairscan/app/data/FileManagerTest.kt b/app/src/test/java/org/fairscan/app/data/FileManagerTest.kt index e4b2122..2312136 100644 --- a/app/src/test/java/org/fairscan/app/data/FileManagerTest.kt +++ b/app/src/test/java/org/fairscan/app/data/FileManagerTest.kt @@ -16,6 +16,7 @@ package org.fairscan.app.data import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat +import org.fairscan.app.domain.Jpeg import org.fairscan.app.domain.JpegProvider import org.junit.Test import java.io.File @@ -74,12 +75,12 @@ class FileManagerTest { val fakePdfWriter = object : PdfWriter { override suspend fun writePdfFromJpegs(jpegs: List, outputStream: OutputStream): Int { val list = jpegs.toList() - list.forEach { bytes -> outputStream.write(bytes.get()) } + list.forEach { bytes -> outputStream.write(bytes.get().bytes) } return list.size } } val manager = FileManager(pdfDir, externalDir, fakePdfWriter) - val jpegs = listOf(byteArrayOf(0x01, 0x02), byteArrayOf(0x11)).map { JpegProvider { it } } + val jpegs = listOf(byteArrayOf(0x01, 0x02), byteArrayOf(0x11)).map { JpegProvider { Jpeg(it) } } val pdf = manager.generatePdf(jpegs) assertThat(pdf.pageCount).isEqualTo(2) assertThat(pdf.sizeInBytes).isEqualTo(3) diff --git a/app/src/test/java/org/fairscan/app/data/ImageRepositoryTest.kt b/app/src/test/java/org/fairscan/app/data/ImageRepositoryTest.kt index 12cfabd..2eb7215 100644 --- a/app/src/test/java/org/fairscan/app/data/ImageRepositoryTest.kt +++ b/app/src/test/java/org/fairscan/app/data/ImageRepositoryTest.kt @@ -19,6 +19,7 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat +import org.fairscan.app.domain.Jpeg import org.fairscan.app.domain.PageMetadata import org.fairscan.app.domain.PageViewKey import org.fairscan.app.domain.Rotation.R0 @@ -53,11 +54,11 @@ class ImageRepositoryTest { fun repo(): ImageRepository { val transformations = object : ImageTransformations { - override fun rotate(input: ByteArray, rotationDegrees: Int, jpegQuality: Int): ByteArray { + override fun rotate(input: Jpeg, rotationDegrees: Int, jpegQuality: Int): Jpeg { return input } - override fun resize(input: ByteArray, maxSize: Int): ByteArray { - return byteArrayOf(input[0]) + override fun resize(input: Jpeg, maxSize: Int): Jpeg { + return jpeg(input.bytes[0]) } } return ImageRepository(getFilesDir(), transformations, 200, testScope) @@ -67,13 +68,13 @@ class ImageRepositoryTest { fun add_image() = runTest { val repo = repo() assertThat(repo.imageIds()).isEmpty() - val bytes = byteArrayOf(101, 102, 103) - repo.add(bytes, byteArrayOf(51), metadata1) + val jpeg = jpeg(101, 102, 103) + repo.add(jpeg, jpeg(51), metadata1) assertThat(repo.imageIds()).hasSize(1) val id = repo.imageIds()[0] val key = PageViewKey(id, R0) - assertThat(repo.jpegBytes(key)).isEqualTo(bytes) - assertThat(repo.getThumbnail(key)).isEqualTo(byteArrayOf(101)) + assertThat(repo.jpegBytes(key)).isEqualTo(jpeg) + assertThat(repo.getThumbnail(key)?.bytes).isEqualTo(byteArrayOf(101)) val page = repo.pages().first() assertThat(page.id).isEqualTo(id) @@ -88,8 +89,8 @@ class ImageRepositoryTest { @Test fun delete_image() = runTest { val repo = repo() - val bytes = byteArrayOf(101, 102, 103) - repo.add(bytes, byteArrayOf(51), metadata1) + val jpeg = jpeg(101, 102, 103) + repo.add(jpeg, jpeg(51), metadata1) assertThat(jpegFiles(processedDir())).hasSize(1) assertThat(jpegFiles(sourceDir())).hasSize(1) assertThat(repo.imageIds()).hasSize(1) @@ -138,7 +139,7 @@ class ImageRepositoryTest { File(processedDir(), "1-90.jpg").writeBytes(bytes) val repo = repo() assertThat(repo.imageIds()).containsExactly("1") - assertThat(repo.jpegBytes(PageViewKey("1", R0))).isEqualTo(bytes) + assertThat(repo.jpegBytes(PageViewKey("1", R0))?.bytes).isEqualTo(bytes) } @Test @@ -167,7 +168,7 @@ class ImageRepositoryTest { File(processedDir(), "1-90.jpg").writeBytes(bytes) val repo = repo() assertThat(repo.imageIds()).containsExactly("1") - assertThat(repo.jpegBytes(PageViewKey("1", R0))).isEqualTo(bytes) + assertThat(repo.jpegBytes(PageViewKey("1", R0))?.bytes).isEqualTo(bytes) } @Test @@ -179,9 +180,9 @@ class ImageRepositoryTest { @Test fun `clear should delete pages`() = runTest { - val bytes = byteArrayOf(101, 102, 103) + val jpeg = jpeg(101, 102, 103) val repo1 = repo() - repo1.add(bytes, byteArrayOf(51), metadata1) + repo1.add(jpeg, jpeg(51), metadata1) assertThat(repo1.imageIds()).isNotEmpty() repo1.clear() assertThat(repo1.imageIds()).isEmpty() @@ -194,7 +195,7 @@ class ImageRepositoryTest { @Test fun rotate() = runTest { val repo = repo() - repo.add(byteArrayOf(101, 102, 103), byteArrayOf(51), metadata1) + repo.add(jpeg(101, 102, 103), jpeg(51), metadata1) assertThat(repo.pages().last().metadata).isEqualTo(metadata1) val id = repo.pages().last().id repo.rotate(id, true) @@ -223,9 +224,9 @@ class ImageRepositoryTest { @Test fun movePage() = runTest { val repo = repo() - repo.add(byteArrayOf(101), byteArrayOf(51), metadata1) + repo.add(jpeg(101), jpeg(51), metadata1) Thread.sleep(1L) // to avoid file name clashes - repo.add(byteArrayOf(110), byteArrayOf(51), metadata1) + repo.add(jpeg(110), jpeg(51), metadata1) val id0 = repo.imageIds().first() val id1 = repo.imageIds().last() repo.movePage(id1, 0) @@ -275,10 +276,10 @@ class ImageRepositoryTest { fun last_added_source_file() = runTest { val repo = repo() assertThat(repo.lastAddedSourceFile()).isNull() - repo.add(byteArrayOf(101), byteArrayOf(51), metadata1) + repo.add(jpeg(101), jpeg(51), metadata1) assertThat(repo.lastAddedSourceFile()).hasBinaryContent(byteArrayOf(51)) Thread.sleep(1) - repo.add(byteArrayOf(102), byteArrayOf(52), metadata1) + repo.add(jpeg(102), jpeg(52), metadata1) assertThat(repo.lastAddedSourceFile()).hasBinaryContent(byteArrayOf(52)) val id = repo.imageIds().last() @@ -307,4 +308,6 @@ class ImageRepositoryTest { suspend fun ImageRepository.imageIds(): PersistentList = pages().map { it.id }.toPersistentList() + + private fun jpeg(vararg bytes: Byte) = Jpeg(bytes) }