New feature: ability to force color mode for a page
This commit is contained in:
@@ -124,8 +124,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
val currentScreen by viewModel.currentScreen.collectAsStateWithLifecycle()
|
val currentScreen by viewModel.currentScreen.collectAsStateWithLifecycle()
|
||||||
val liveAnalysisState by cameraViewModel.liveAnalysisState.collectAsStateWithLifecycle()
|
val liveAnalysisState by cameraViewModel.liveAnalysisState.collectAsStateWithLifecycle()
|
||||||
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
|
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
|
||||||
val currentPageIndex by viewModel.currentPageIndex.collectAsStateWithLifecycle()
|
val documentUiState by viewModel.documentUiState.collectAsStateWithLifecycle()
|
||||||
val currentPageBitmap by viewModel.currentPageBitmap.collectAsStateWithLifecycle()
|
|
||||||
val exportUiState by exportViewModel.uiState.collectAsStateWithLifecycle()
|
val exportUiState by exportViewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val cameraPermission = rememberCameraPermissionState()
|
val cameraPermission = rememberCameraPermissionState()
|
||||||
CollectCameraEvents(cameraViewModel, viewModel)
|
CollectCameraEvents(cameraViewModel, viewModel)
|
||||||
@@ -180,11 +179,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
is Screen.Main.Document -> {
|
is Screen.Main.Document -> {
|
||||||
DocumentScreen (
|
DocumentScreen (
|
||||||
uiState = DocumentUiState(currentPageIndex, currentPageBitmap, document),
|
uiState = documentUiState,
|
||||||
navigation = navigation,
|
navigation = navigation,
|
||||||
onExportClick = onExportClick,
|
onExportClick = onExportClick,
|
||||||
onDeleteImage = { id -> viewModel.deletePage(id) },
|
onDeleteImage = { id -> viewModel.deletePage(id) },
|
||||||
onRotateImage = { id, clockwise -> viewModel.rotateImage(id, clockwise) },
|
onRotateImage = { id, clockwise -> viewModel.rotateImage(id, clockwise) },
|
||||||
|
onToggleColorMode = { id -> viewModel.togglePageColorMode(id) },
|
||||||
onPageReorder = { id, newIndex -> viewModel.movePage(id, newIndex) },
|
onPageReorder = { id, newIndex -> viewModel.movePage(id, newIndex) },
|
||||||
onPageSelected = viewModel::onPageSelected
|
onPageSelected = viewModel::onPageSelected
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,13 +15,13 @@
|
|||||||
package org.fairscan.app
|
package org.fairscan.app
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
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.domain.ScanPage
|
||||||
import org.fairscan.app.ui.NavigationState
|
import org.fairscan.app.ui.NavigationState
|
||||||
import org.fairscan.app.ui.Screen
|
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.DocumentUiModel
|
||||||
import org.fairscan.app.ui.state.PageThumbnail
|
import org.fairscan.app.ui.state.PageThumbnail
|
||||||
|
import org.fairscan.imageprocessing.ColorMode
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode): ViewModel() {
|
class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode): ViewModel() {
|
||||||
|
|
||||||
private val _navigationState = MutableStateFlow(NavigationState.initial(launchMode))
|
private val _navigationState = MutableStateFlow(NavigationState.initial(launchMode))
|
||||||
@@ -68,21 +72,31 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
|||||||
.stateIn(
|
.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
started = SharingStarted.Eagerly,
|
started = SharingStarted.Eagerly,
|
||||||
initialValue = DocumentUiModel(persistentListOf())
|
initialValue = DocumentUiModel()
|
||||||
)
|
)
|
||||||
|
|
||||||
private val _currentPageIndex = MutableStateFlow(0)
|
private val _currentPageIndex = MutableStateFlow(0)
|
||||||
val currentPageIndex: StateFlow<Int> =
|
|
||||||
_currentPageIndex.stateIn(viewModelScope, SharingStarted.Eagerly, 0)
|
val currentPageUiState: Flow<CurrentPageUiState> =
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
combine(_currentPageIndex, _pages) { index, pages -> pages.getOrNull(index) }
|
||||||
val currentPageBitmap: StateFlow<Bitmap?> =
|
|
||||||
_currentPageIndex
|
|
||||||
.combine(_pages) { index, pages -> pages.getOrNull(index) }
|
|
||||||
.mapLatest { page ->
|
.mapLatest { page ->
|
||||||
page?.let { imageRepository.jpegBytes(it.key())?.toBitmap() }
|
page?.let {
|
||||||
|
CurrentPageUiState(
|
||||||
|
imageRepository.jpegBytes(it.key())?.toBitmap(),
|
||||||
|
page.colorMode
|
||||||
|
)
|
||||||
|
} ?: CurrentPageUiState()
|
||||||
}
|
}
|
||||||
.flowOn(Dispatchers.IO)
|
.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) {
|
fun onPageSelected(index: Int) {
|
||||||
_currentPageIndex.value = index
|
_currentPageIndex.value = index
|
||||||
@@ -90,6 +104,9 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
|||||||
|
|
||||||
fun navigateTo(destination: Screen) {
|
fun navigateTo(destination: Screen) {
|
||||||
if (destination is Screen.Main.Document) {
|
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)
|
_currentPageIndex.value = min(_pages.value.size - 1, destination.initialPage)
|
||||||
}
|
}
|
||||||
_navigationState.update { it.navigateTo(destination) }
|
_navigationState.update { it.navigateTo(destination) }
|
||||||
@@ -125,7 +142,6 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
|||||||
imageRepository.delete(id)
|
imageRepository.delete(id)
|
||||||
imageRepository.pages()
|
imageRepository.pages()
|
||||||
}
|
}
|
||||||
_pages.value = pages
|
|
||||||
|
|
||||||
if (pages.isEmpty()) {
|
if (pages.isEmpty()) {
|
||||||
navigateTo(Screen.Main.Camera)
|
navigateTo(Screen.Main.Camera)
|
||||||
@@ -133,6 +149,22 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
|||||||
} else if (_currentPageIndex.value >= pages.size) {
|
} else if (_currentPageIndex.value >= pages.size) {
|
||||||
_currentPageIndex.value = pages.size - 1
|
_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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
package org.fairscan.app.data
|
package org.fairscan.app.data
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.fairscan.imageprocessing.ColorMode
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class DocumentMetadataV1(
|
data class DocumentMetadataV1(
|
||||||
@@ -39,7 +40,8 @@ data class PageV2(
|
|||||||
val baseRotationDegrees: Int = 0,
|
val baseRotationDegrees: Int = 0,
|
||||||
val manualRotationDegrees: Int = 0,
|
val manualRotationDegrees: Int = 0,
|
||||||
val quad: NormalizedQuad? = null,
|
val quad: NormalizedQuad? = null,
|
||||||
val isColored: Boolean? = null
|
val isColored: Boolean? = null,
|
||||||
|
val colorMode: ColorMode? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import org.fairscan.imageprocessing.ColorMode
|
|||||||
import org.fairscan.imageprocessing.Point
|
import org.fairscan.imageprocessing.Point
|
||||||
import org.fairscan.imageprocessing.Quad
|
import org.fairscan.imageprocessing.Quad
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Collections
|
import java.util.Collections.synchronizedMap
|
||||||
|
|
||||||
const val SOURCE_DIR_NAME = "sources"
|
const val SOURCE_DIR_NAME = "sources"
|
||||||
const val PROCESSED_DIR_NAME = "scanned_pages"
|
const val PROCESSED_DIR_NAME = "scanned_pages"
|
||||||
@@ -65,11 +65,12 @@ class ImageRepository(
|
|||||||
private val json = Json { prettyPrint = false; encodeDefaults = true }
|
private val json = Json { prettyPrint = false; encodeDefaults = true }
|
||||||
private var pages: PageStore = PageStore(loadPages())
|
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 imageCache = createLruCache<PageViewKey, Deferred<Jpeg?>>(maxEntries = 50)
|
||||||
private val thumbnailCache = createLruCache<PageViewKey, Deferred<Jpeg?>>(maxEntries = 200)
|
private val thumbnailCache = createLruCache<PageViewKey, Deferred<Jpeg?>>(maxEntries = 200)
|
||||||
|
|
||||||
private fun <K, V> createLruCache(maxEntries: Int): MutableMap<K, V> =
|
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
|
override fun removeEldestEntry(eldest: Map.Entry<K, V>) = size > maxEntries
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ class ImageRepository(
|
|||||||
return when {
|
return when {
|
||||||
metadataPages != null ->
|
metadataPages != null ->
|
||||||
metadataPages
|
metadataPages
|
||||||
.filter { "${it.id}.jpg" in filesOnDisk }
|
.filter { processedImageFileName(it.id, it.colorMode) in filesOnDisk }
|
||||||
.toMutableList()
|
.toMutableList()
|
||||||
else ->
|
else ->
|
||||||
filesOnDisk
|
filesOnDisk
|
||||||
@@ -135,7 +136,7 @@ class ImageRepository(
|
|||||||
pages.pages().mapNotNull {
|
pages.pages().mapNotNull {
|
||||||
runCatching {
|
runCatching {
|
||||||
val manualRotation = Rotation.fromDegrees(it.manualRotationDegrees)
|
val manualRotation = Rotation.fromDegrees(it.manualRotationDegrees)
|
||||||
ScanPage(it.id, manualRotation, it.toMetadata())
|
ScanPage(it.id, manualRotation, it.colorMode, it.toMetadata())
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,24 +144,53 @@ class ImageRepository(
|
|||||||
suspend fun add(processed: Jpeg, source: Jpeg, metadata: PageMetadata) =
|
suspend fun add(processed: Jpeg, source: Jpeg, metadata: PageMetadata) =
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
val id = "${System.currentTimeMillis()}"
|
val id = "${System.currentTimeMillis()}"
|
||||||
val fileName = "$id.jpg"
|
val key = PageViewKey(id, Rotation.R0, metadata.autoColorMode)
|
||||||
File(processedDir, fileName).writeBytes(processed.bytes)
|
processedImageFile(key).writeBytes(processed.bytes)
|
||||||
File(sourceDir, fileName).writeBytes(source.bytes)
|
sourceFile(id).writeBytes(source.bytes)
|
||||||
pages.addOrReplace(
|
pages.addOrReplace(
|
||||||
PageV2(
|
PageV2(
|
||||||
id = id,
|
id = id,
|
||||||
quad = metadata.normalizedQuad.toSerializable(),
|
quad = metadata.normalizedQuad.toSerializable(),
|
||||||
baseRotationDegrees = metadata.baseRotation.degrees,
|
baseRotationDegrees = metadata.baseRotation.degrees,
|
||||||
manualRotationDegrees = Rotation.R0.degrees,
|
manualRotationDegrees = Rotation.R0.degrees,
|
||||||
isColored = metadata.autoColorMode == ColorMode.COLOR
|
isColored = metadata.autoColorMode == ColorMode.COLOR,
|
||||||
|
colorMode = metadata.autoColorMode,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
saveMetadata()
|
saveMetadata()
|
||||||
// Pre-populate cache for R0
|
// Pre-populate cache for R0
|
||||||
val key = PageViewKey(id, Rotation.R0)
|
|
||||||
imageCache.put(key, CompletableDeferred(processed))
|
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 {
|
suspend fun rotate(id: String, clockwise: Boolean) = mutex.withLock {
|
||||||
val page = pages.get(id) ?: return@withLock
|
val page = pages.get(id) ?: return@withLock
|
||||||
val delta = if (clockwise) Rotation.R90 else Rotation.R270
|
val delta = if (clockwise) Rotation.R90 else Rotation.R270
|
||||||
@@ -198,7 +228,7 @@ class ImageRepository(
|
|||||||
|
|
||||||
private suspend fun computeProcessedImage(key: PageViewKey): Jpeg? =
|
private suspend fun computeProcessedImage(key: PageViewKey): Jpeg? =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val baseFile = File(processedDir, "${key.pageId}.jpg")
|
val baseFile = processedImageFile(key)
|
||||||
if (!baseFile.exists()) return@withContext null
|
if (!baseFile.exists()) return@withContext null
|
||||||
val baseJpeg = Jpeg(baseFile.readBytes())
|
val baseJpeg = Jpeg(baseFile.readBytes())
|
||||||
if (key.rotation == Rotation.R0) {
|
if (key.rotation == Rotation.R0) {
|
||||||
@@ -220,8 +250,20 @@ class ImageRepository(
|
|||||||
|
|
||||||
// --- Other operations ---
|
// --- 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? {
|
fun source(id: String): Jpeg? {
|
||||||
val file = File(sourceDir, "$id.jpg")
|
val file = sourceFile(id)
|
||||||
return if (file.exists()) Jpeg(file.readBytes()) else null
|
return if (file.exists()) Jpeg(file.readBytes()) else null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +275,7 @@ class ImageRepository(
|
|||||||
suspend fun delete(id: String) = mutex.withLock {
|
suspend fun delete(id: String) = mutex.withLock {
|
||||||
pages.delete(id)
|
pages.delete(id)
|
||||||
saveMetadata()
|
saveMetadata()
|
||||||
File(sourceDir, "$id.jpg").delete()
|
sourceFile(id).delete()
|
||||||
processedDir.listFiles()
|
processedDir.listFiles()
|
||||||
?.filter { it.name.startsWith("$id.") || it.name.startsWith("$id-") }
|
?.filter { it.name.startsWith("$id.") || it.name.startsWith("$id-") }
|
||||||
?.forEach { it.delete() }
|
?.forEach { it.delete() }
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
package org.fairscan.app.data
|
package org.fairscan.app.data
|
||||||
|
|
||||||
import org.fairscan.app.domain.Jpeg
|
import org.fairscan.app.domain.Jpeg
|
||||||
|
import org.fairscan.app.domain.PageMetadata
|
||||||
|
import org.fairscan.imageprocessing.ColorMode
|
||||||
|
|
||||||
interface ImageTransformations {
|
interface ImageTransformations {
|
||||||
|
|
||||||
@@ -22,4 +24,6 @@ interface ImageTransformations {
|
|||||||
|
|
||||||
fun resize(input: Jpeg, maxSize: Int): Jpeg
|
fun resize(input: Jpeg, maxSize: Int): Jpeg
|
||||||
|
|
||||||
|
fun process(source: Jpeg, metadata: PageMetadata, colorMode: ColorMode): Jpeg
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
package org.fairscan.app.domain
|
package org.fairscan.app.domain
|
||||||
|
|
||||||
import org.fairscan.app.data.ImageRepository
|
import org.fairscan.app.data.ImageRepository
|
||||||
|
import org.fairscan.imageprocessing.ColorMode
|
||||||
import org.fairscan.imageprocessing.extractDocument
|
import org.fairscan.imageprocessing.extractDocument
|
||||||
import org.fairscan.imageprocessing.resizeForMaxPixels
|
import org.fairscan.imageprocessing.resizeForMaxPixels
|
||||||
import org.fairscan.imageprocessing.scaledTo
|
import org.fairscan.imageprocessing.scaledTo
|
||||||
@@ -48,10 +49,11 @@ suspend fun jpegsForExport(
|
|||||||
ExportQuality.HIGH -> pages.map { page ->
|
ExportQuality.HIGH -> pages.map { page ->
|
||||||
JpegProvider {
|
JpegProvider {
|
||||||
val source = imageRepository.source(page.id)
|
val source = imageRepository.source(page.id)
|
||||||
val pageMetadata = page.metadata
|
val metadata = page.metadata
|
||||||
val manualRotation = page.manualRotation
|
val manualRotation = page.manualRotation
|
||||||
if (source != null && pageMetadata != null)
|
val colorMode = page.colorMode
|
||||||
prepareJpegForHigh(source, pageMetadata, manualRotation, exportQuality)
|
if (source != null && metadata != null && colorMode != null)
|
||||||
|
prepareJpegForHigh(source, metadata, manualRotation, colorMode, exportQuality)
|
||||||
else
|
else
|
||||||
jpeg(page, imageRepository)
|
jpeg(page, imageRepository)
|
||||||
}
|
}
|
||||||
@@ -86,6 +88,7 @@ private fun prepareJpegForHigh(
|
|||||||
source: Jpeg,
|
source: Jpeg,
|
||||||
pageMetadata: PageMetadata,
|
pageMetadata: PageMetadata,
|
||||||
manualRotation: Rotation,
|
manualRotation: Rotation,
|
||||||
|
colorMode: ColorMode,
|
||||||
exportQuality: ExportQuality,
|
exportQuality: ExportQuality,
|
||||||
): Jpeg {
|
): Jpeg {
|
||||||
|
|
||||||
@@ -94,13 +97,8 @@ private fun prepareJpegForHigh(
|
|||||||
try {
|
try {
|
||||||
decoded = source.toMat()
|
decoded = source.toMat()
|
||||||
val quad = pageMetadata.normalizedQuad.scaledTo(1, 1, decoded.width(), decoded.height())
|
val quad = pageMetadata.normalizedQuad.scaledTo(1, 1, decoded.width(), decoded.height())
|
||||||
page = extractDocument(
|
val rotationDegrees = pageMetadata.baseRotation.add(manualRotation).degrees
|
||||||
decoded,
|
page = extractDocument(decoded, quad, rotationDegrees, colorMode, exportQuality.maxPixels)
|
||||||
quad,
|
|
||||||
pageMetadata.baseRotation.add(manualRotation).degrees,
|
|
||||||
pageMetadata.autoColorMode,
|
|
||||||
exportQuality.maxPixels
|
|
||||||
)
|
|
||||||
return Jpeg.fromMat(page, exportQuality.jpegQuality)
|
return Jpeg.fromMat(page, exportQuality.jpegQuality)
|
||||||
} finally {
|
} finally {
|
||||||
decoded?.release()
|
decoded?.release()
|
||||||
|
|||||||
@@ -26,16 +26,18 @@ data class PageMetadata(
|
|||||||
data class ScanPage(
|
data class ScanPage(
|
||||||
val id: String,
|
val id: String,
|
||||||
val manualRotation: Rotation,
|
val manualRotation: Rotation,
|
||||||
|
val colorMode: ColorMode?,
|
||||||
val metadata: PageMetadata?,
|
val metadata: PageMetadata?,
|
||||||
) {
|
) {
|
||||||
fun key(): PageViewKey = PageViewKey(id, manualRotation)
|
fun key(): PageViewKey = PageViewKey(id, manualRotation, colorMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PageViewKey(
|
data class PageViewKey(
|
||||||
val pageId: String,
|
val pageId: String,
|
||||||
val rotation: Rotation,
|
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) {
|
enum class Rotation(val degrees: Int) {
|
||||||
|
|||||||
@@ -15,7 +15,12 @@
|
|||||||
package org.fairscan.app.platform
|
package org.fairscan.app.platform
|
||||||
|
|
||||||
import org.fairscan.app.data.ImageTransformations
|
import org.fairscan.app.data.ImageTransformations
|
||||||
|
import org.fairscan.app.domain.ExportQuality
|
||||||
import org.fairscan.app.domain.Jpeg
|
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.Mat
|
||||||
import org.opencv.core.Size
|
import org.opencv.core.Size
|
||||||
import org.opencv.imgproc.Imgproc
|
import org.opencv.imgproc.Imgproc
|
||||||
@@ -59,4 +64,30 @@ class OpenCvTransformations : ImageTransformations {
|
|||||||
output?.release()
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import org.fairscan.app.domain.PageViewKey
|
|||||||
import org.fairscan.app.domain.Rotation
|
import org.fairscan.app.domain.Rotation
|
||||||
import org.fairscan.app.ui.state.DocumentUiModel
|
import org.fairscan.app.ui.state.DocumentUiModel
|
||||||
import org.fairscan.app.ui.state.PageThumbnail
|
import org.fairscan.app.ui.state.PageThumbnail
|
||||||
|
import org.fairscan.imageprocessing.ColorMode
|
||||||
|
|
||||||
fun dummyNavigation(): Navigation {
|
fun dummyNavigation(): Navigation {
|
||||||
return Navigation({}, {}, {}, {}, {}, {}, {}, {})
|
return Navigation({}, {}, {}, {}, {}, {}, {}, {})
|
||||||
@@ -35,7 +36,7 @@ fun fakeDocument(): DocumentUiModel {
|
|||||||
|
|
||||||
fun fakeDocument(pageIds: ImmutableList<String>, context: Context): DocumentUiModel {
|
fun fakeDocument(pageIds: ImmutableList<String>, context: Context): DocumentUiModel {
|
||||||
val pageKeys = pageIds.map {
|
val pageKeys = pageIds.map {
|
||||||
PageThumbnail(PageViewKey(it, Rotation.R0), fakeImage(it, context))
|
PageThumbnail(PageViewKey(it, Rotation.R0, ColorMode.COLOR), fakeImage(it, context))
|
||||||
}.toImmutableList()
|
}.toImmutableList()
|
||||||
return DocumentUiModel(pageKeys)
|
return DocumentUiModel(pageKeys)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ import androidx.compose.foundation.layout.width
|
|||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
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.Done
|
||||||
|
import androidx.compose.material.icons.filled.Palette
|
||||||
import androidx.compose.material.icons.filled.RotateLeft
|
import androidx.compose.material.icons.filled.RotateLeft
|
||||||
import androidx.compose.material.icons.filled.RotateRight
|
import androidx.compose.material.icons.filled.RotateRight
|
||||||
import androidx.compose.material.icons.outlined.Add
|
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.fakeDocument
|
||||||
import org.fairscan.app.ui.fakeImage
|
import org.fairscan.app.ui.fakeImage
|
||||||
import org.fairscan.app.ui.theme.FairScanTheme
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -83,6 +88,7 @@ fun DocumentScreen(
|
|||||||
onExportClick: () -> Unit,
|
onExportClick: () -> Unit,
|
||||||
onDeleteImage: (String) -> Unit,
|
onDeleteImage: (String) -> Unit,
|
||||||
onRotateImage: (String, Boolean) -> Unit,
|
onRotateImage: (String, Boolean) -> Unit,
|
||||||
|
onToggleColorMode: (String) -> Unit,
|
||||||
onPageReorder: (String, Int) -> Unit,
|
onPageReorder: (String, Int) -> Unit,
|
||||||
onPageSelected: (Int) -> Unit,
|
onPageSelected: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -116,6 +122,7 @@ fun DocumentScreen(
|
|||||||
uiState,
|
uiState,
|
||||||
{ showDeletePageDialog.value = true },
|
{ showDeletePageDialog.value = true },
|
||||||
onRotateImage,
|
onRotateImage,
|
||||||
|
onToggleColorMode,
|
||||||
modifier
|
modifier
|
||||||
)
|
)
|
||||||
if (showDeletePageDialog.value) {
|
if (showDeletePageDialog.value) {
|
||||||
@@ -133,6 +140,7 @@ private fun DocumentPreview(
|
|||||||
uiState: DocumentUiState,
|
uiState: DocumentUiState,
|
||||||
onDeleteImage: (String) -> Unit,
|
onDeleteImage: (String) -> Unit,
|
||||||
onRotateImage: (String, Boolean) -> Unit,
|
onRotateImage: (String, Boolean) -> Unit,
|
||||||
|
onToggleColorMode: (String) -> Unit,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
) {
|
) {
|
||||||
val currentPageIndex = uiState.currentPageIndex
|
val currentPageIndex = uiState.currentPageIndex
|
||||||
@@ -145,7 +153,7 @@ private fun DocumentPreview(
|
|||||||
Box (
|
Box (
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
val bitmap = uiState.currentPageBitmap
|
val bitmap = uiState.currentPage.bitmap
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
val imageBitmap = bitmap.asImageBitmap()
|
val imageBitmap = bitmap.asImageBitmap()
|
||||||
val zoomState = remember(imageId) {
|
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))
|
RotationButtons(imageId, onRotateImage, Modifier.align(Alignment.BottomCenter))
|
||||||
SecondaryActionButton(
|
SecondaryActionButton(
|
||||||
Icons.Outlined.Delete,
|
Icons.Outlined.Delete,
|
||||||
@@ -179,10 +196,10 @@ private fun DocumentPreview(
|
|||||||
Text("${currentPageIndex + 1} / ${document.pageCount()}",
|
Text("${currentPageIndex + 1} / ${document.pageCount()}",
|
||||||
color = MaterialTheme.colorScheme.inverseOnSurface,
|
color = MaterialTheme.colorScheme.inverseOnSurface,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomStart)
|
.align(Alignment.TopCenter)
|
||||||
.padding(all = 16.dp)
|
.padding(all = 16.dp)
|
||||||
.background(
|
.background(
|
||||||
color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.5f),
|
color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.7f),
|
||||||
shape = RoundedCornerShape(4.dp)
|
shape = RoundedCornerShape(4.dp)
|
||||||
)
|
)
|
||||||
.padding(horizontal = 8.dp, vertical = 2.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
|
// RotateLeft on the left, RotateRight on the right: for both LTR and RTL languages
|
||||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
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
|
// Using AutoMirrored icons would lead to an opposite rotation in RTL languages
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
SecondaryActionButton(
|
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
|
@Composable
|
||||||
private fun BottomBar(
|
private fun BottomBar(
|
||||||
onExportClick: () -> Unit,
|
onExportClick: () -> Unit,
|
||||||
@@ -264,11 +307,12 @@ fun DocumentScreenPreview() {
|
|||||||
LocalContext.current
|
LocalContext.current
|
||||||
)
|
)
|
||||||
DocumentScreen(
|
DocumentScreen(
|
||||||
uiState = DocumentUiState(1, image, document),
|
uiState = DocumentUiState(1, CurrentPageUiState(image, COLOR), document),
|
||||||
navigation = dummyNavigation(),
|
navigation = dummyNavigation(),
|
||||||
onExportClick = {},
|
onExportClick = {},
|
||||||
onDeleteImage = { _ -> },
|
onDeleteImage = { _ -> },
|
||||||
onRotateImage = { _,_ -> },
|
onRotateImage = { _,_ -> },
|
||||||
|
onToggleColorMode = { _ -> },
|
||||||
onPageReorder = { _,_ -> },
|
onPageReorder = { _,_ -> },
|
||||||
onPageSelected = { _ -> },
|
onPageSelected = { _ -> },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,9 +16,15 @@ package org.fairscan.app.ui.screens.document
|
|||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import org.fairscan.app.ui.state.DocumentUiModel
|
import org.fairscan.app.ui.state.DocumentUiModel
|
||||||
|
import org.fairscan.imageprocessing.ColorMode
|
||||||
|
|
||||||
data class DocumentUiState(
|
data class DocumentUiState(
|
||||||
val currentPageIndex: Int,
|
val currentPageIndex: Int,
|
||||||
val currentPageBitmap: Bitmap?,
|
val currentPage: CurrentPageUiState,
|
||||||
val document: DocumentUiModel,
|
val document: DocumentUiModel,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class CurrentPageUiState(
|
||||||
|
val bitmap: Bitmap? = null,
|
||||||
|
val colorMode: ColorMode? = null,
|
||||||
|
)
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ package org.fairscan.app.ui.state
|
|||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import org.fairscan.app.domain.PageViewKey
|
import org.fairscan.app.domain.PageViewKey
|
||||||
|
|
||||||
data class DocumentUiModel(
|
data class DocumentUiModel(
|
||||||
val pages: ImmutableList<PageThumbnail>,
|
val pages: ImmutableList<PageThumbnail> = persistentListOf(),
|
||||||
) {
|
) {
|
||||||
fun pageCount(): Int {
|
fun pageCount(): Int {
|
||||||
return pages.size
|
return pages.size
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<string name="cancel">ألغِ</string>
|
<string name="cancel">ألغِ</string>
|
||||||
<string name="change_directory">غيّر المجلد</string>
|
<string name="change_directory">غيّر المجلد</string>
|
||||||
<string name="clear_text">امحُ النص</string>
|
<string name="clear_text">امحُ النص</string>
|
||||||
|
<string name="color_mode_color">ألوان</string>
|
||||||
|
<string name="color_mode_grayscale">تدرج الرمادي</string>
|
||||||
<string name="contact">تواصل</string>
|
<string name="contact">تواصل</string>
|
||||||
<string name="copied_logs">نُسخت السجلات إلى الحافظة</string>
|
<string name="copied_logs">نُسخت السجلات إلى الحافظة</string>
|
||||||
<string name="copy_logs">انسخ السجلات</string>
|
<string name="copy_logs">انسخ السجلات</string>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<string name="cancel">Zrušit</string>
|
<string name="cancel">Zrušit</string>
|
||||||
<string name="change_directory">Změnit složku</string>
|
<string name="change_directory">Změnit složku</string>
|
||||||
<string name="clear_text">Smazat text</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="contact">Kontakt</string>
|
||||||
<string name="copied_logs">Protokoly zkopírovány do schránky</string>
|
<string name="copied_logs">Protokoly zkopírovány do schránky</string>
|
||||||
<string name="copy_logs">Kopírovat protokoly</string>
|
<string name="copy_logs">Kopírovat protokoly</string>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<string name="cancel">Abbrechen</string>
|
<string name="cancel">Abbrechen</string>
|
||||||
<string name="change_directory">Ordner ändern</string>
|
<string name="change_directory">Ordner ändern</string>
|
||||||
<string name="clear_text">Text löschen</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="contact">Kontakt</string>
|
||||||
<string name="copied_logs">Logs in die Zwischenablage kopiert</string>
|
<string name="copied_logs">Logs in die Zwischenablage kopiert</string>
|
||||||
<string name="copy_logs">Logs kopieren</string>
|
<string name="copy_logs">Logs kopieren</string>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<string name="cancel">Cancelar</string>
|
<string name="cancel">Cancelar</string>
|
||||||
<string name="change_directory">Cambiar carpeta</string>
|
<string name="change_directory">Cambiar carpeta</string>
|
||||||
<string name="clear_text">Borrar texto</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="contact">Contacto</string>
|
||||||
<string name="copied_logs">Registros copiados al portapapeles</string>
|
<string name="copied_logs">Registros copiados al portapapeles</string>
|
||||||
<string name="copy_logs">Copiar registros</string>
|
<string name="copy_logs">Copiar registros</string>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<string name="cancel">Annuler</string>
|
<string name="cancel">Annuler</string>
|
||||||
<string name="change_directory">Changer de dossier</string>
|
<string name="change_directory">Changer de dossier</string>
|
||||||
<string name="clear_text">Effacer le text</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="contact">Contact</string>
|
||||||
<string name="copied_logs">Logs copiés dans le presse-papiers</string>
|
<string name="copied_logs">Logs copiés dans le presse-papiers</string>
|
||||||
<string name="copy_logs">Copier les logs</string>
|
<string name="copy_logs">Copier les logs</string>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<string name="cancel">Cancelar</string>
|
<string name="cancel">Cancelar</string>
|
||||||
<string name="change_directory">Trocar cartafol</string>
|
<string name="change_directory">Trocar cartafol</string>
|
||||||
<string name="clear_text">Borrar texto</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="contact">Contacto</string>
|
||||||
<string name="copied_logs">Rexistros copiados ao portapapeis</string>
|
<string name="copied_logs">Rexistros copiados ao portapapeis</string>
|
||||||
<string name="copy_logs">Copiar rexistros</string>
|
<string name="copy_logs">Copiar rexistros</string>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<string name="cancel">Annulla</string>
|
<string name="cancel">Annulla</string>
|
||||||
<string name="change_directory">Cambia cartella</string>
|
<string name="change_directory">Cambia cartella</string>
|
||||||
<string name="clear_text">Svuota testo</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="contact">Contatti</string>
|
||||||
<string name="copied_logs">Log copiati negli appunti</string>
|
<string name="copied_logs">Log copiati negli appunti</string>
|
||||||
<string name="copy_logs">Copia log</string>
|
<string name="copy_logs">Copia log</string>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<string name="cancel">Cancelar</string>
|
<string name="cancel">Cancelar</string>
|
||||||
<string name="change_directory">Alterar diretório</string>
|
<string name="change_directory">Alterar diretório</string>
|
||||||
<string name="clear_text">Limpar texto</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="contact">Contato</string>
|
||||||
<string name="copied_logs">Registros copiados para a área de transferência</string>
|
<string name="copied_logs">Registros copiados para a área de transferência</string>
|
||||||
<string name="copy_logs">Copiar registros</string>
|
<string name="copy_logs">Copiar registros</string>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<string name="cancel">Отмена</string>
|
<string name="cancel">Отмена</string>
|
||||||
<string name="change_directory">Изменить папку</string>
|
<string name="change_directory">Изменить папку</string>
|
||||||
<string name="clear_text">Стереть текст</string>
|
<string name="clear_text">Стереть текст</string>
|
||||||
|
<string name="color_mode_color">Цвет</string>
|
||||||
|
<string name="color_mode_grayscale">Оттенки серого</string>
|
||||||
<string name="contact">Контакты</string>
|
<string name="contact">Контакты</string>
|
||||||
<string name="copied_logs">Журналы скопированы в буфер обмена</string>
|
<string name="copied_logs">Журналы скопированы в буфер обмена</string>
|
||||||
<string name="copy_logs">Копировать журналы</string>
|
<string name="copy_logs">Копировать журналы</string>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<string name="cancel">İptal</string>
|
<string name="cancel">İptal</string>
|
||||||
<string name="change_directory">Dizini değiştir</string>
|
<string name="change_directory">Dizini değiştir</string>
|
||||||
<string name="clear_text">Metni temizle</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="contact">İletişim</string>
|
||||||
<string name="copied_logs">Günlükler panoya kopyalandı</string>
|
<string name="copied_logs">Günlükler panoya kopyalandı</string>
|
||||||
<string name="copy_logs">Günlükleri kopyala</string>
|
<string name="copy_logs">Günlükleri kopyala</string>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<string name="cancel">取消</string>
|
<string name="cancel">取消</string>
|
||||||
<string name="change_directory">變更目錄</string>
|
<string name="change_directory">變更目錄</string>
|
||||||
<string name="clear_text">清除文字</string>
|
<string name="clear_text">清除文字</string>
|
||||||
|
<string name="color_mode_color">彩色</string>
|
||||||
|
<string name="color_mode_grayscale">灰階</string>
|
||||||
<string name="contact">聯絡我們</string>
|
<string name="contact">聯絡我們</string>
|
||||||
<string name="copied_logs">日誌已複製到剪貼簿</string>
|
<string name="copied_logs">日誌已複製到剪貼簿</string>
|
||||||
<string name="copy_logs">複製日誌</string>
|
<string name="copy_logs">複製日誌</string>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<string name="cancel">取消</string>
|
<string name="cancel">取消</string>
|
||||||
<string name="change_directory">更改目录</string>
|
<string name="change_directory">更改目录</string>
|
||||||
<string name="clear_text">清除文字</string>
|
<string name="clear_text">清除文字</string>
|
||||||
|
<string name="color_mode_color">彩色</string>
|
||||||
|
<string name="color_mode_grayscale">灰度</string>
|
||||||
<string name="contact">联系人</string>
|
<string name="contact">联系人</string>
|
||||||
<string name="copied_logs">日志已复制到剪贴板</string>
|
<string name="copied_logs">日志已复制到剪贴板</string>
|
||||||
<string name="copy_logs">复制日志</string>
|
<string name="copy_logs">复制日志</string>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
<string name="change_directory">Change folder</string>
|
<string name="change_directory">Change folder</string>
|
||||||
<string name="clear_text">Clear text</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="contact">Contact</string>
|
||||||
<string name="copied_logs">Logs copied to clipboard</string>
|
<string name="copied_logs">Logs copied to clipboard</string>
|
||||||
<string name="copy_logs">Copy logs</string>
|
<string name="copy_logs">Copy logs</string>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import org.fairscan.app.domain.Rotation.R180
|
|||||||
import org.fairscan.app.domain.Rotation.R270
|
import org.fairscan.app.domain.Rotation.R270
|
||||||
import org.fairscan.app.domain.Rotation.R90
|
import org.fairscan.app.domain.Rotation.R90
|
||||||
import org.fairscan.imageprocessing.ColorMode
|
import org.fairscan.imageprocessing.ColorMode
|
||||||
|
import org.fairscan.imageprocessing.ColorMode.COLOR
|
||||||
import org.fairscan.imageprocessing.Point
|
import org.fairscan.imageprocessing.Point
|
||||||
import org.fairscan.imageprocessing.Quad
|
import org.fairscan.imageprocessing.Quad
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
@@ -44,7 +45,7 @@ class ImageRepositoryTest {
|
|||||||
private val testScope = TestScope()
|
private val testScope = TestScope()
|
||||||
|
|
||||||
val quad1 = Quad(Point(.01, .02), Point(.1, .03), Point(.11, .12), Point(.03, .09))
|
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 {
|
fun getFilesDir(): File {
|
||||||
if (_filesDir == null) {
|
if (_filesDir == null) {
|
||||||
@@ -61,6 +62,10 @@ class ImageRepositoryTest {
|
|||||||
override fun resize(input: Jpeg, maxSize: Int): Jpeg {
|
override fun resize(input: Jpeg, maxSize: Int): Jpeg {
|
||||||
return jpeg(input.bytes[0])
|
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)
|
return ImageRepository(getFilesDir(), transformations, 200, testScope)
|
||||||
}
|
}
|
||||||
@@ -73,7 +78,7 @@ class ImageRepositoryTest {
|
|||||||
repo.add(jpeg, jpeg(51), metadata1)
|
repo.add(jpeg, jpeg(51), metadata1)
|
||||||
assertThat(repo.imageIds()).hasSize(1)
|
assertThat(repo.imageIds()).hasSize(1)
|
||||||
val id = repo.imageIds()[0]
|
val id = repo.imageIds()[0]
|
||||||
val key = PageViewKey(id, R0)
|
val key = PageViewKey(id, R0, COLOR)
|
||||||
assertThat(repo.jpegBytes(key)).isEqualTo(jpeg)
|
assertThat(repo.jpegBytes(key)).isEqualTo(jpeg)
|
||||||
assertThat(repo.getThumbnail(key)?.bytes).isEqualTo(byteArrayOf(101))
|
assertThat(repo.getThumbnail(key)?.bytes).isEqualTo(byteArrayOf(101))
|
||||||
|
|
||||||
@@ -140,7 +145,7 @@ class ImageRepositoryTest {
|
|||||||
File(processedDir(), "1-90.jpg").writeBytes(bytes)
|
File(processedDir(), "1-90.jpg").writeBytes(bytes)
|
||||||
val repo = repo()
|
val repo = repo()
|
||||||
assertThat(repo.imageIds()).containsExactly("1")
|
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
|
@Test
|
||||||
@@ -169,14 +174,14 @@ class ImageRepositoryTest {
|
|||||||
File(processedDir(), "1-90.jpg").writeBytes(bytes)
|
File(processedDir(), "1-90.jpg").writeBytes(bytes)
|
||||||
val repo = repo()
|
val repo = repo()
|
||||||
assertThat(repo.imageIds()).containsExactly("1")
|
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
|
@Test
|
||||||
fun `should return null on invalid id`() = runTest {
|
fun `should return null on invalid id`() = runTest {
|
||||||
val repo = repo()
|
val repo = repo()
|
||||||
assertThat(repo.imageIds()).isEmpty()
|
assertThat(repo.imageIds()).isEmpty()
|
||||||
assertThat(repo.jpegBytes(PageViewKey("x", R0))).isNull()
|
assertThat(repo.jpegBytes(PageViewKey("x", R0, COLOR))).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -255,7 +260,7 @@ class ImageRepositoryTest {
|
|||||||
val metadata = PageV2("1", 0, 0, quad, isColored).toMetadata()
|
val metadata = PageV2("1", 0, 0, quad, isColored).toMetadata()
|
||||||
assertThat(metadata).isNotNull()
|
assertThat(metadata).isNotNull()
|
||||||
assertThat(metadata!!.autoColorMode).isEqualTo(
|
assertThat(metadata!!.autoColorMode).isEqualTo(
|
||||||
if (isColored) ColorMode.COLOR else ColorMode.GRAYSCALE
|
if (isColored) COLOR else ColorMode.GRAYSCALE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user