New feature: ability to force color mode for a page

This commit is contained in:
Pierre-Yves Nicolas
2026-03-30 20:48:58 +02:00
parent 7d01493477
commit b258082ce1
26 changed files with 247 additions and 53 deletions

View File

@@ -124,8 +124,7 @@ class MainActivity : ComponentActivity() {
val currentScreen by viewModel.currentScreen.collectAsStateWithLifecycle()
val liveAnalysisState by cameraViewModel.liveAnalysisState.collectAsStateWithLifecycle()
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
val currentPageIndex by viewModel.currentPageIndex.collectAsStateWithLifecycle()
val currentPageBitmap by viewModel.currentPageBitmap.collectAsStateWithLifecycle()
val documentUiState by viewModel.documentUiState.collectAsStateWithLifecycle()
val exportUiState by exportViewModel.uiState.collectAsStateWithLifecycle()
val cameraPermission = rememberCameraPermissionState()
CollectCameraEvents(cameraViewModel, viewModel)
@@ -180,11 +179,12 @@ class MainActivity : ComponentActivity() {
}
is Screen.Main.Document -> {
DocumentScreen (
uiState = DocumentUiState(currentPageIndex, currentPageBitmap, document),
uiState = documentUiState,
navigation = navigation,
onExportClick = onExportClick,
onDeleteImage = { id -> viewModel.deletePage(id) },
onRotateImage = { id, clockwise -> viewModel.rotateImage(id, clockwise) },
onToggleColorMode = { id -> viewModel.togglePageColorMode(id) },
onPageReorder = { id, newIndex -> viewModel.movePage(id, newIndex) },
onPageSelected = viewModel::onPageSelected
)

View File

@@ -15,13 +15,13 @@
package org.fairscan.app
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -38,10 +38,14 @@ import org.fairscan.app.domain.CapturedPage
import org.fairscan.app.domain.ScanPage
import org.fairscan.app.ui.NavigationState
import org.fairscan.app.ui.Screen
import org.fairscan.app.ui.screens.document.CurrentPageUiState
import org.fairscan.app.ui.screens.document.DocumentUiState
import org.fairscan.app.ui.state.DocumentUiModel
import org.fairscan.app.ui.state.PageThumbnail
import org.fairscan.imageprocessing.ColorMode
import kotlin.math.min
@OptIn(ExperimentalCoroutinesApi::class)
class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode): ViewModel() {
private val _navigationState = MutableStateFlow(NavigationState.initial(launchMode))
@@ -68,21 +72,31 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = DocumentUiModel(persistentListOf())
initialValue = DocumentUiModel()
)
private val _currentPageIndex = MutableStateFlow(0)
val currentPageIndex: StateFlow<Int> =
_currentPageIndex.stateIn(viewModelScope, SharingStarted.Eagerly, 0)
@OptIn(ExperimentalCoroutinesApi::class)
val currentPageBitmap: StateFlow<Bitmap?> =
_currentPageIndex
.combine(_pages) { index, pages -> pages.getOrNull(index) }
val currentPageUiState: Flow<CurrentPageUiState> =
combine(_currentPageIndex, _pages) { index, pages -> pages.getOrNull(index) }
.mapLatest { page ->
page?.let { imageRepository.jpegBytes(it.key())?.toBitmap() }
page?.let {
CurrentPageUiState(
imageRepository.jpegBytes(it.key())?.toBitmap(),
page.colorMode
)
} ?: CurrentPageUiState()
}
.flowOn(Dispatchers.IO)
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
val documentUiState: StateFlow<DocumentUiState> =
combine(_currentPageIndex, currentPageUiState, documentUiModel) { index, page, document ->
DocumentUiState(index, page, document)
}
.stateIn(
viewModelScope, SharingStarted.Eagerly,
DocumentUiState(0, CurrentPageUiState(), DocumentUiModel())
)
fun onPageSelected(index: Int) {
_currentPageIndex.value = index
@@ -90,6 +104,9 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
fun navigateTo(destination: Screen) {
if (destination is Screen.Main.Document) {
require(_pages.value.isNotEmpty()) {
"Cannot navigate to DocumentScreen with zero pages"
}
_currentPageIndex.value = min(_pages.value.size - 1, destination.initialPage)
}
_navigationState.update { it.navigateTo(destination) }
@@ -125,7 +142,6 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
imageRepository.delete(id)
imageRepository.pages()
}
_pages.value = pages
if (pages.isEmpty()) {
navigateTo(Screen.Main.Camera)
@@ -133,6 +149,22 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
} else if (_currentPageIndex.value >= pages.size) {
_currentPageIndex.value = pages.size - 1
}
_pages.value = pages
}
}
fun togglePageColorMode(id: String) {
viewModelScope.launch {
val currentColorMode = _pages.value.find { p -> p.id == id }?.colorMode
currentColorMode?.let {
val newColorMode =
if (it == ColorMode.COLOR) ColorMode.GRAYSCALE else ColorMode.COLOR
val pages = withContext(Dispatchers.IO) {
imageRepository.setColorMode(id, newColorMode)
imageRepository.pages()
}
_pages.value = pages
}
}
}

View File

@@ -15,6 +15,7 @@
package org.fairscan.app.data
import kotlinx.serialization.Serializable
import org.fairscan.imageprocessing.ColorMode
@Serializable
data class DocumentMetadataV1(
@@ -39,7 +40,8 @@ data class PageV2(
val baseRotationDegrees: Int = 0,
val manualRotationDegrees: Int = 0,
val quad: NormalizedQuad? = null,
val isColored: Boolean? = null
val isColored: Boolean? = null,
val colorMode: ColorMode? = null,
)
@Serializable

View File

@@ -37,7 +37,7 @@ import org.fairscan.imageprocessing.ColorMode
import org.fairscan.imageprocessing.Point
import org.fairscan.imageprocessing.Quad
import java.io.File
import java.util.Collections
import java.util.Collections.synchronizedMap
const val SOURCE_DIR_NAME = "sources"
const val PROCESSED_DIR_NAME = "scanned_pages"
@@ -65,11 +65,12 @@ class ImageRepository(
private val json = Json { prettyPrint = false; encodeDefaults = true }
private var pages: PageStore = PageStore(loadPages())
private val processingJobs = synchronizedMap(mutableMapOf<PageViewKey, Deferred<Unit>>())
private val imageCache = createLruCache<PageViewKey, Deferred<Jpeg?>>(maxEntries = 50)
private val thumbnailCache = createLruCache<PageViewKey, Deferred<Jpeg?>>(maxEntries = 200)
private fun <K, V> createLruCache(maxEntries: Int): MutableMap<K, V> =
Collections.synchronizedMap(object : LinkedHashMap<K, V>(16, 0.75f, true) {
synchronizedMap(object : LinkedHashMap<K, V>(16, 0.75f, true) {
override fun removeEldestEntry(eldest: Map.Entry<K, V>) = size > maxEntries
})
@@ -91,7 +92,7 @@ class ImageRepository(
return when {
metadataPages != null ->
metadataPages
.filter { "${it.id}.jpg" in filesOnDisk }
.filter { processedImageFileName(it.id, it.colorMode) in filesOnDisk }
.toMutableList()
else ->
filesOnDisk
@@ -135,7 +136,7 @@ class ImageRepository(
pages.pages().mapNotNull {
runCatching {
val manualRotation = Rotation.fromDegrees(it.manualRotationDegrees)
ScanPage(it.id, manualRotation, it.toMetadata())
ScanPage(it.id, manualRotation, it.colorMode, it.toMetadata())
}.getOrNull()
}
}
@@ -143,24 +144,53 @@ class ImageRepository(
suspend fun add(processed: Jpeg, source: Jpeg, metadata: PageMetadata) =
mutex.withLock {
val id = "${System.currentTimeMillis()}"
val fileName = "$id.jpg"
File(processedDir, fileName).writeBytes(processed.bytes)
File(sourceDir, fileName).writeBytes(source.bytes)
val key = PageViewKey(id, Rotation.R0, metadata.autoColorMode)
processedImageFile(key).writeBytes(processed.bytes)
sourceFile(id).writeBytes(source.bytes)
pages.addOrReplace(
PageV2(
id = id,
quad = metadata.normalizedQuad.toSerializable(),
baseRotationDegrees = metadata.baseRotation.degrees,
manualRotationDegrees = Rotation.R0.degrees,
isColored = metadata.autoColorMode == ColorMode.COLOR
isColored = metadata.autoColorMode == ColorMode.COLOR,
colorMode = metadata.autoColorMode,
)
)
saveMetadata()
// Pre-populate cache for R0
val key = PageViewKey(id, Rotation.R0)
imageCache.put(key, CompletableDeferred(processed))
}
suspend fun setColorMode(id: String, colorMode: ColorMode) {
val key = PageViewKey(id, Rotation.R0, colorMode)
val processedFile = processedImageFile(key)
val metadata = mutex.withLock { pages.get(id)?.toMetadata() }
val sourceFile = sourceFile(id)
if (metadata == null || !sourceFile.exists())
return
val job = processingJobs.computeIfAbsent(key) {
scope.async(Dispatchers.IO) {
if (!processedFile.exists()) {
val sourceJpeg = Jpeg(sourceFile.readBytes())
val processedJpeg = transformations.process(sourceJpeg, metadata, colorMode)
processedFile.writeBytes(processedJpeg.bytes)
}
}
}
try {
job.await()
} finally {
processingJobs.remove(key, job)
}
mutex.withLock {
pages.update(id) { it.copy(colorMode = colorMode) }
saveMetadata()
}
}
suspend fun rotate(id: String, clockwise: Boolean) = mutex.withLock {
val page = pages.get(id) ?: return@withLock
val delta = if (clockwise) Rotation.R90 else Rotation.R270
@@ -198,7 +228,7 @@ class ImageRepository(
private suspend fun computeProcessedImage(key: PageViewKey): Jpeg? =
withContext(Dispatchers.IO) {
val baseFile = File(processedDir, "${key.pageId}.jpg")
val baseFile = processedImageFile(key)
if (!baseFile.exists()) return@withContext null
val baseJpeg = Jpeg(baseFile.readBytes())
if (key.rotation == Rotation.R0) {
@@ -220,8 +250,20 @@ class ImageRepository(
// --- Other operations ---
private fun processedImageFileName(id: String, colorMode: ColorMode?) : String =
if (colorMode == null)
"${id}.jpg"
else
"${id}.${colorMode.name.lowercase()}.jpg"
private fun processedImageFile(key: PageViewKey) : File =
File(processedDir, processedImageFileName(key.pageId, key.colorMode))
private fun sourceFile(id: String): File =
File(sourceDir, "$id.jpg")
fun source(id: String): Jpeg? {
val file = File(sourceDir, "$id.jpg")
val file = sourceFile(id)
return if (file.exists()) Jpeg(file.readBytes()) else null
}
@@ -233,7 +275,7 @@ class ImageRepository(
suspend fun delete(id: String) = mutex.withLock {
pages.delete(id)
saveMetadata()
File(sourceDir, "$id.jpg").delete()
sourceFile(id).delete()
processedDir.listFiles()
?.filter { it.name.startsWith("$id.") || it.name.startsWith("$id-") }
?.forEach { it.delete() }

View File

@@ -15,6 +15,8 @@
package org.fairscan.app.data
import org.fairscan.app.domain.Jpeg
import org.fairscan.app.domain.PageMetadata
import org.fairscan.imageprocessing.ColorMode
interface ImageTransformations {
@@ -22,4 +24,6 @@ interface ImageTransformations {
fun resize(input: Jpeg, maxSize: Int): Jpeg
fun process(source: Jpeg, metadata: PageMetadata, colorMode: ColorMode): Jpeg
}

View File

@@ -15,6 +15,7 @@
package org.fairscan.app.domain
import org.fairscan.app.data.ImageRepository
import org.fairscan.imageprocessing.ColorMode
import org.fairscan.imageprocessing.extractDocument
import org.fairscan.imageprocessing.resizeForMaxPixels
import org.fairscan.imageprocessing.scaledTo
@@ -48,10 +49,11 @@ suspend fun jpegsForExport(
ExportQuality.HIGH -> pages.map { page ->
JpegProvider {
val source = imageRepository.source(page.id)
val pageMetadata = page.metadata
val metadata = page.metadata
val manualRotation = page.manualRotation
if (source != null && pageMetadata != null)
prepareJpegForHigh(source, pageMetadata, manualRotation, exportQuality)
val colorMode = page.colorMode
if (source != null && metadata != null && colorMode != null)
prepareJpegForHigh(source, metadata, manualRotation, colorMode, exportQuality)
else
jpeg(page, imageRepository)
}
@@ -86,6 +88,7 @@ private fun prepareJpegForHigh(
source: Jpeg,
pageMetadata: PageMetadata,
manualRotation: Rotation,
colorMode: ColorMode,
exportQuality: ExportQuality,
): Jpeg {
@@ -94,13 +97,8 @@ private fun prepareJpegForHigh(
try {
decoded = source.toMat()
val quad = pageMetadata.normalizedQuad.scaledTo(1, 1, decoded.width(), decoded.height())
page = extractDocument(
decoded,
quad,
pageMetadata.baseRotation.add(manualRotation).degrees,
pageMetadata.autoColorMode,
exportQuality.maxPixels
)
val rotationDegrees = pageMetadata.baseRotation.add(manualRotation).degrees
page = extractDocument(decoded, quad, rotationDegrees, colorMode, exportQuality.maxPixels)
return Jpeg.fromMat(page, exportQuality.jpegQuality)
} finally {
decoded?.release()

View File

@@ -26,16 +26,18 @@ data class PageMetadata(
data class ScanPage(
val id: String,
val manualRotation: Rotation,
val colorMode: ColorMode?,
val metadata: PageMetadata?,
) {
fun key(): PageViewKey = PageViewKey(id, manualRotation)
fun key(): PageViewKey = PageViewKey(id, manualRotation, colorMode)
}
data class PageViewKey(
val pageId: String,
val rotation: Rotation,
val colorMode: ColorMode?,
) {
val saveKey: String get() = "$pageId-${rotation.degrees}"
val saveKey: String get() = "$pageId-${rotation.degrees}-$colorMode"
}
enum class Rotation(val degrees: Int) {

View File

@@ -15,7 +15,12 @@
package org.fairscan.app.platform
import org.fairscan.app.data.ImageTransformations
import org.fairscan.app.domain.ExportQuality
import org.fairscan.app.domain.Jpeg
import org.fairscan.app.domain.PageMetadata
import org.fairscan.imageprocessing.ColorMode
import org.fairscan.imageprocessing.extractDocument
import org.fairscan.imageprocessing.scaledTo
import org.opencv.core.Mat
import org.opencv.core.Size
import org.opencv.imgproc.Imgproc
@@ -59,4 +64,30 @@ class OpenCvTransformations : ImageTransformations {
output?.release()
}
}
override fun process(source: Jpeg, metadata: PageMetadata, colorMode: ColorMode): Jpeg {
val exportQuality = ExportQuality.BALANCED
var sourceMat: Mat? = null
var page: Mat? = null
try {
sourceMat = source.toMat()
val quad = metadata.normalizedQuad.scaledTo(
1,
1,
sourceMat.width(),
sourceMat.height()
)
page = extractDocument(
sourceMat,
quad,
metadata.baseRotation.degrees,
colorMode,
exportQuality.maxPixels
)
return Jpeg.fromMat(page, exportQuality.jpegQuality)
} finally {
sourceMat?.release()
page?.release()
}
}
}

View File

@@ -24,6 +24,7 @@ import org.fairscan.app.domain.PageViewKey
import org.fairscan.app.domain.Rotation
import org.fairscan.app.ui.state.DocumentUiModel
import org.fairscan.app.ui.state.PageThumbnail
import org.fairscan.imageprocessing.ColorMode
fun dummyNavigation(): Navigation {
return Navigation({}, {}, {}, {}, {}, {}, {}, {})
@@ -35,7 +36,7 @@ fun fakeDocument(): DocumentUiModel {
fun fakeDocument(pageIds: ImmutableList<String>, context: Context): DocumentUiModel {
val pageKeys = pageIds.map {
PageThumbnail(PageViewKey(it, Rotation.R0), fakeImage(it, context))
PageThumbnail(PageViewKey(it, Rotation.R0, ColorMode.COLOR), fakeImage(it, context))
}.toImmutableList()
return DocumentUiModel(pageKeys)
}

View File

@@ -31,7 +31,9 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Contrast
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.RotateLeft
import androidx.compose.material.icons.filled.RotateRight
import androidx.compose.material.icons.outlined.Add
@@ -74,6 +76,9 @@ import org.fairscan.app.ui.dummyNavigation
import org.fairscan.app.ui.fakeDocument
import org.fairscan.app.ui.fakeImage
import org.fairscan.app.ui.theme.FairScanTheme
import org.fairscan.imageprocessing.ColorMode
import org.fairscan.imageprocessing.ColorMode.COLOR
import org.fairscan.imageprocessing.ColorMode.GRAYSCALE
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -83,6 +88,7 @@ fun DocumentScreen(
onExportClick: () -> Unit,
onDeleteImage: (String) -> Unit,
onRotateImage: (String, Boolean) -> Unit,
onToggleColorMode: (String) -> Unit,
onPageReorder: (String, Int) -> Unit,
onPageSelected: (Int) -> Unit,
) {
@@ -116,6 +122,7 @@ fun DocumentScreen(
uiState,
{ showDeletePageDialog.value = true },
onRotateImage,
onToggleColorMode,
modifier
)
if (showDeletePageDialog.value) {
@@ -133,6 +140,7 @@ private fun DocumentPreview(
uiState: DocumentUiState,
onDeleteImage: (String) -> Unit,
onRotateImage: (String, Boolean) -> Unit,
onToggleColorMode: (String) -> Unit,
modifier: Modifier,
) {
val currentPageIndex = uiState.currentPageIndex
@@ -145,7 +153,7 @@ private fun DocumentPreview(
Box (
modifier = Modifier.fillMaxSize()
) {
val bitmap = uiState.currentPageBitmap
val bitmap = uiState.currentPage.bitmap
if (bitmap != null) {
val imageBitmap = bitmap.asImageBitmap()
val zoomState = remember(imageId) {
@@ -167,6 +175,15 @@ private fun DocumentPreview(
)
}
}
uiState.currentPage.colorMode?.let {
ColorModeButton(
currentColorMode = it,
onToggle = { onToggleColorMode(imageId) },
modifier = Modifier
.align(Alignment.BottomStart)
.padding(8.dp)
)
}
RotationButtons(imageId, onRotateImage, Modifier.align(Alignment.BottomCenter))
SecondaryActionButton(
Icons.Outlined.Delete,
@@ -179,10 +196,10 @@ private fun DocumentPreview(
Text("${currentPageIndex + 1} / ${document.pageCount()}",
color = MaterialTheme.colorScheme.inverseOnSurface,
modifier = Modifier
.align(Alignment.BottomStart)
.align(Alignment.TopCenter)
.padding(all = 16.dp)
.background(
color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.5f),
color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.7f),
shape = RoundedCornerShape(4.dp)
)
.padding(horizontal = 8.dp, vertical = 2.dp)
@@ -199,7 +216,7 @@ fun RotationButtons(
) {
// RotateLeft on the left, RotateRight on the right: for both LTR and RTL languages
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
Row(modifier = modifier.padding(4.dp)) {
Row(modifier = modifier.padding(8.dp)) {
// Using AutoMirrored icons would lead to an opposite rotation in RTL languages
@Suppress("DEPRECATION")
SecondaryActionButton(
@@ -218,6 +235,32 @@ fun RotationButtons(
}
}
@Composable
fun ColorModeButton(
currentColorMode: ColorMode,
onToggle: () -> Unit,
modifier: Modifier = Modifier
) {
val icon = when (currentColorMode) {
COLOR -> Icons.Default.Palette
GRAYSCALE -> Icons.Default.Contrast
}
val label = when (currentColorMode) {
COLOR -> stringResource(R.string.color_mode_color)
GRAYSCALE -> stringResource(R.string.color_mode_grayscale)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
) {
SecondaryActionButton(
icon = icon,
contentDescription = label,
onClick = onToggle,
)
}
}
@Composable
private fun BottomBar(
onExportClick: () -> Unit,
@@ -264,11 +307,12 @@ fun DocumentScreenPreview() {
LocalContext.current
)
DocumentScreen(
uiState = DocumentUiState(1, image, document),
uiState = DocumentUiState(1, CurrentPageUiState(image, COLOR), document),
navigation = dummyNavigation(),
onExportClick = {},
onDeleteImage = { _ -> },
onRotateImage = { _,_ -> },
onToggleColorMode = { _ -> },
onPageReorder = { _,_ -> },
onPageSelected = { _ -> },
)

View File

@@ -16,9 +16,15 @@ package org.fairscan.app.ui.screens.document
import android.graphics.Bitmap
import org.fairscan.app.ui.state.DocumentUiModel
import org.fairscan.imageprocessing.ColorMode
data class DocumentUiState(
val currentPageIndex: Int,
val currentPageBitmap: Bitmap?,
val currentPage: CurrentPageUiState,
val document: DocumentUiModel,
)
data class CurrentPageUiState(
val bitmap: Bitmap? = null,
val colorMode: ColorMode? = null,
)

View File

@@ -16,10 +16,11 @@ package org.fairscan.app.ui.state
import android.graphics.Bitmap
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import org.fairscan.app.domain.PageViewKey
data class DocumentUiModel(
val pages: ImmutableList<PageThumbnail>,
val pages: ImmutableList<PageThumbnail> = persistentListOf(),
) {
fun pageCount(): Int {
return pages.size

View File

@@ -8,6 +8,8 @@
<string name="cancel">ألغِ</string>
<string name="change_directory">غيّر المجلد</string>
<string name="clear_text">امحُ النص</string>
<string name="color_mode_color">ألوان</string>
<string name="color_mode_grayscale">تدرج الرمادي</string>
<string name="contact">تواصل</string>
<string name="copied_logs">نُسخت السجلات إلى الحافظة</string>
<string name="copy_logs">انسخ السجلات</string>

View File

@@ -8,6 +8,8 @@
<string name="cancel">Zrušit</string>
<string name="change_directory">Změnit složku</string>
<string name="clear_text">Smazat text</string>
<string name="color_mode_color">Barva</string>
<string name="color_mode_grayscale">Odstíny šedi</string>
<string name="contact">Kontakt</string>
<string name="copied_logs">Protokoly zkopírovány do schránky</string>
<string name="copy_logs">Kopírovat protokoly</string>

View File

@@ -8,6 +8,8 @@
<string name="cancel">Abbrechen</string>
<string name="change_directory">Ordner ändern</string>
<string name="clear_text">Text löschen</string>
<string name="color_mode_color">Farbe</string>
<string name="color_mode_grayscale">Graustufen</string>
<string name="contact">Kontakt</string>
<string name="copied_logs">Logs in die Zwischenablage kopiert</string>
<string name="copy_logs">Logs kopieren</string>

View File

@@ -8,6 +8,8 @@
<string name="cancel">Cancelar</string>
<string name="change_directory">Cambiar carpeta</string>
<string name="clear_text">Borrar texto</string>
<string name="color_mode_color">Color</string>
<string name="color_mode_grayscale">Escala de grises</string>
<string name="contact">Contacto</string>
<string name="copied_logs">Registros copiados al portapapeles</string>
<string name="copy_logs">Copiar registros</string>

View File

@@ -8,6 +8,8 @@
<string name="cancel">Annuler</string>
<string name="change_directory">Changer de dossier</string>
<string name="clear_text">Effacer le text</string>
<string name="color_mode_color">Couleur</string>
<string name="color_mode_grayscale">Niveaux de gris</string>
<string name="contact">Contact</string>
<string name="copied_logs">Logs copiés dans le presse-papiers</string>
<string name="copy_logs">Copier les logs</string>

View File

@@ -8,6 +8,8 @@
<string name="cancel">Cancelar</string>
<string name="change_directory">Trocar cartafol</string>
<string name="clear_text">Borrar texto</string>
<string name="color_mode_color">Cor</string>
<string name="color_mode_grayscale">Escala de grises</string>
<string name="contact">Contacto</string>
<string name="copied_logs">Rexistros copiados ao portapapeis</string>
<string name="copy_logs">Copiar rexistros</string>

View File

@@ -8,6 +8,8 @@
<string name="cancel">Annulla</string>
<string name="change_directory">Cambia cartella</string>
<string name="clear_text">Svuota testo</string>
<string name="color_mode_color">Colore</string>
<string name="color_mode_grayscale">Scala di grigi</string>
<string name="contact">Contatti</string>
<string name="copied_logs">Log copiati negli appunti</string>
<string name="copy_logs">Copia log</string>

View File

@@ -8,6 +8,8 @@
<string name="cancel">Cancelar</string>
<string name="change_directory">Alterar diretório</string>
<string name="clear_text">Limpar texto</string>
<string name="color_mode_color">Cor</string>
<string name="color_mode_grayscale">Escala de cinza</string>
<string name="contact">Contato</string>
<string name="copied_logs">Registros copiados para a área de transferência</string>
<string name="copy_logs">Copiar registros</string>

View File

@@ -8,6 +8,8 @@
<string name="cancel">Отмена</string>
<string name="change_directory">Изменить папку</string>
<string name="clear_text">Стереть текст</string>
<string name="color_mode_color">Цвет</string>
<string name="color_mode_grayscale">Оттенки серого</string>
<string name="contact">Контакты</string>
<string name="copied_logs">Журналы скопированы в буфер обмена</string>
<string name="copy_logs">Копировать журналы</string>

View File

@@ -8,6 +8,8 @@
<string name="cancel">İptal</string>
<string name="change_directory">Dizini değiştir</string>
<string name="clear_text">Metni temizle</string>
<string name="color_mode_color">Renkli</string>
<string name="color_mode_grayscale">Gri tonlama</string>
<string name="contact">İletişim</string>
<string name="copied_logs">Günlükler panoya kopyalandı</string>
<string name="copy_logs">Günlükleri kopyala</string>

View File

@@ -8,6 +8,8 @@
<string name="cancel">取消</string>
<string name="change_directory">變更目錄</string>
<string name="clear_text">清除文字</string>
<string name="color_mode_color">彩色</string>
<string name="color_mode_grayscale">灰階</string>
<string name="contact">聯絡我們</string>
<string name="copied_logs">日誌已複製到剪貼簿</string>
<string name="copy_logs">複製日誌</string>

View File

@@ -8,6 +8,8 @@
<string name="cancel">取消</string>
<string name="change_directory">更改目录</string>
<string name="clear_text">清除文字</string>
<string name="color_mode_color">彩色</string>
<string name="color_mode_grayscale">灰度</string>
<string name="contact">联系人</string>
<string name="copied_logs">日志已复制到剪贴板</string>
<string name="copy_logs">复制日志</string>

View File

@@ -9,6 +9,8 @@
<string name="cancel">Cancel</string>
<string name="change_directory">Change folder</string>
<string name="clear_text">Clear text</string>
<string name="color_mode_color">Color</string>
<string name="color_mode_grayscale">Grayscale</string>
<string name="contact">Contact</string>
<string name="copied_logs">Logs copied to clipboard</string>
<string name="copy_logs">Copy logs</string>

View File

@@ -27,6 +27,7 @@ import org.fairscan.app.domain.Rotation.R180
import org.fairscan.app.domain.Rotation.R270
import org.fairscan.app.domain.Rotation.R90
import org.fairscan.imageprocessing.ColorMode
import org.fairscan.imageprocessing.ColorMode.COLOR
import org.fairscan.imageprocessing.Point
import org.fairscan.imageprocessing.Quad
import org.junit.Rule
@@ -44,7 +45,7 @@ class ImageRepositoryTest {
private val testScope = TestScope()
val quad1 = Quad(Point(.01, .02), Point(.1, .03), Point(.11, .12), Point(.03, .09))
val metadata1 = PageMetadata(quad1, R90, ColorMode.COLOR)
val metadata1 = PageMetadata(quad1, R90, COLOR)
fun getFilesDir(): File {
if (_filesDir == null) {
@@ -61,6 +62,10 @@ class ImageRepositoryTest {
override fun resize(input: Jpeg, maxSize: Int): Jpeg {
return jpeg(input.bytes[0])
}
override fun process(source: Jpeg, metadata: PageMetadata, colorMode: ColorMode): Jpeg {
TODO("Not yet implemented")
}
}
return ImageRepository(getFilesDir(), transformations, 200, testScope)
}
@@ -73,7 +78,7 @@ class ImageRepositoryTest {
repo.add(jpeg, jpeg(51), metadata1)
assertThat(repo.imageIds()).hasSize(1)
val id = repo.imageIds()[0]
val key = PageViewKey(id, R0)
val key = PageViewKey(id, R0, COLOR)
assertThat(repo.jpegBytes(key)).isEqualTo(jpeg)
assertThat(repo.getThumbnail(key)?.bytes).isEqualTo(byteArrayOf(101))
@@ -140,7 +145,7 @@ class ImageRepositoryTest {
File(processedDir(), "1-90.jpg").writeBytes(bytes)
val repo = repo()
assertThat(repo.imageIds()).containsExactly("1")
assertThat(repo.jpegBytes(PageViewKey("1", R0))?.bytes).isEqualTo(bytes)
assertThat(repo.jpegBytes(PageViewKey("1", R0, null))?.bytes).isEqualTo(bytes)
}
@Test
@@ -169,14 +174,14 @@ class ImageRepositoryTest {
File(processedDir(), "1-90.jpg").writeBytes(bytes)
val repo = repo()
assertThat(repo.imageIds()).containsExactly("1")
assertThat(repo.jpegBytes(PageViewKey("1", R0))?.bytes).isEqualTo(bytes)
assertThat(repo.jpegBytes(PageViewKey("1", R0, null))?.bytes).isEqualTo(bytes)
}
@Test
fun `should return null on invalid id`() = runTest {
val repo = repo()
assertThat(repo.imageIds()).isEmpty()
assertThat(repo.jpegBytes(PageViewKey("x", R0))).isNull()
assertThat(repo.jpegBytes(PageViewKey("x", R0, COLOR))).isNull()
}
@Test
@@ -255,7 +260,7 @@ class ImageRepositoryTest {
val metadata = PageV2("1", 0, 0, quad, isColored).toMetadata()
assertThat(metadata).isNotNull()
assertThat(metadata!!.autoColorMode).isEqualTo(
if (isColored) ColorMode.COLOR else ColorMode.GRAYSCALE
if (isColored) COLOR else ColorMode.GRAYSCALE
)
}
}