Revamp ImageRepository
This commit is contained in:
@@ -19,6 +19,7 @@ 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.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
|
||||||
@@ -28,6 +29,8 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.fairscan.app.data.ImageRepository
|
import org.fairscan.app.data.ImageRepository
|
||||||
import org.fairscan.app.domain.CapturedPage
|
import org.fairscan.app.domain.CapturedPage
|
||||||
|
import org.fairscan.app.domain.PageViewKey
|
||||||
|
import org.fairscan.app.domain.Rotation
|
||||||
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.state.DocumentUiModel
|
import org.fairscan.app.ui.state.DocumentUiModel
|
||||||
@@ -39,11 +42,13 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
|||||||
val currentScreen: StateFlow<Screen> = _navigationState.map { it.current }
|
val currentScreen: StateFlow<Screen> = _navigationState.map { it.current }
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, _navigationState.value.current)
|
.stateIn(viewModelScope, SharingStarted.Eagerly, _navigationState.value.current)
|
||||||
|
|
||||||
private val _pageIds = MutableStateFlow(imageRepository.imageIds())
|
private val _pages = MutableStateFlow(imageRepository.pages())
|
||||||
val documentUiModel: StateFlow<DocumentUiModel> =
|
val documentUiModel: StateFlow<DocumentUiModel> =
|
||||||
_pageIds.map { ids ->
|
_pages.map { pages ->
|
||||||
DocumentUiModel(
|
DocumentUiModel(
|
||||||
pageIds = ids,
|
pageKeys = pages.map { p ->
|
||||||
|
PageViewKey(p.id, p.metadata?.manualRotation?: Rotation.R0)
|
||||||
|
}.toImmutableList(),
|
||||||
imageLoader = ::getBitmap,
|
imageLoader = ::getBitmap,
|
||||||
thumbnailLoader = ::getThumbnail,
|
thumbnailLoader = ::getThumbnail,
|
||||||
)
|
)
|
||||||
@@ -64,38 +69,38 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
|||||||
fun rotateImage(id: String, clockwise: Boolean) {
|
fun rotateImage(id: String, clockwise: Boolean) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
imageRepository.rotate(id, clockwise)
|
imageRepository.rotate(id, clockwise)
|
||||||
_pageIds.value = imageRepository.imageIds()
|
_pages.value = imageRepository.pages()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun movePage(id: String, newIndex: Int) {
|
fun movePage(id: String, newIndex: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
imageRepository.movePage(id, newIndex)
|
imageRepository.movePage(id, newIndex)
|
||||||
_pageIds.value = imageRepository.imageIds()
|
_pages.value = imageRepository.pages()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deletePage(id: String) {
|
fun deletePage(id: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
imageRepository.delete(id)
|
imageRepository.delete(id)
|
||||||
_pageIds.value = imageRepository.imageIds()
|
_pages.value = imageRepository.pages()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startNewDocument() {
|
fun startNewDocument() {
|
||||||
_pageIds.value = persistentListOf()
|
_pages.value = persistentListOf()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
imageRepository.clear()
|
imageRepository.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBitmap(id: String): Bitmap? {
|
fun getBitmap(key: PageViewKey): Bitmap? {
|
||||||
val bytes = imageRepository.getContent(id)
|
val bytes = imageRepository.jpegBytes(key)
|
||||||
return bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
return bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getThumbnail(id: String): Bitmap? {
|
fun getThumbnail(key: PageViewKey): Bitmap? {
|
||||||
val bytes = imageRepository.getThumbnail(id)
|
val bytes = imageRepository.getThumbnail(key)
|
||||||
return bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
return bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +111,7 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
|||||||
compressJpeg(capturedPage.source, 90),
|
compressJpeg(capturedPage.source, 90),
|
||||||
capturedPage.metadata,
|
capturedPage.metadata,
|
||||||
)
|
)
|
||||||
_pageIds.value = imageRepository.imageIds()
|
_pages.value = imageRepository.pages()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,16 +17,28 @@ package org.fairscan.app.data
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class DocumentMetadata(
|
data class DocumentMetadataV1(
|
||||||
val version: Int = 1,
|
val version: Int = 1,
|
||||||
val pages: List<Page>
|
val pages: List<PageV1>
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Page(
|
data class PageV1(
|
||||||
val file: String,
|
val file: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class DocumentMetadataV2(
|
||||||
|
val version: Int = 2,
|
||||||
|
val pages: List<PageV2>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PageV2(
|
||||||
|
val id: String,
|
||||||
|
val baseRotationDegrees: Int = 0,
|
||||||
|
val manualRotationDegrees: Int = 0,
|
||||||
val quad: NormalizedQuad? = null,
|
val quad: NormalizedQuad? = null,
|
||||||
val rotationDegrees: Int = 0,
|
|
||||||
val isColored: Boolean? = null
|
val isColored: Boolean? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,15 @@
|
|||||||
*/
|
*/
|
||||||
package org.fairscan.app.data
|
package org.fairscan.app.data
|
||||||
|
|
||||||
import kotlinx.collections.immutable.PersistentList
|
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import org.fairscan.app.domain.PageMetadata
|
import org.fairscan.app.domain.PageMetadata
|
||||||
|
import org.fairscan.app.domain.PageViewKey
|
||||||
|
import org.fairscan.app.domain.Rotation
|
||||||
|
import org.fairscan.app.domain.ScanPage
|
||||||
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
|
||||||
@@ -46,127 +51,186 @@ class ImageRepository(
|
|||||||
|
|
||||||
private val metadataFile = File(scanDir, "document.json")
|
private val metadataFile = File(scanDir, "document.json")
|
||||||
|
|
||||||
private var pages: MutableList<Page> = loadPages()
|
private var pages: MutableList<PageV2> = loadPages()
|
||||||
|
|
||||||
private fun loadPages(): MutableList<Page> {
|
private val json = Json {
|
||||||
|
prettyPrint = false
|
||||||
|
encodeDefaults = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadPages(): MutableList<PageV2> {
|
||||||
val filesOnDisk = scanDir.listFiles()
|
val filesOnDisk = scanDir.listFiles()
|
||||||
?.filter { it.extension == "jpg" }
|
?.filter { it.extension == "jpg" }
|
||||||
?.map { it.name }
|
?.map { it.name }
|
||||||
?.toSet()
|
?.toSet()
|
||||||
?: emptySet()
|
?: emptySet()
|
||||||
|
|
||||||
val metadataPages = loadMetadata()?.pages
|
val metadataPages = if (metadataFile.exists()) {
|
||||||
|
runCatching { loadMetadata() }.getOrNull()
|
||||||
|
} else null
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
metadataPages != null ->
|
metadataPages != null ->
|
||||||
metadataPages
|
metadataPages
|
||||||
.filter { it.file in filesOnDisk }
|
.filter { it.workFileName() in filesOnDisk }
|
||||||
.toMutableList()
|
.toMutableList()
|
||||||
else ->
|
else ->
|
||||||
filesOnDisk
|
filesOnDisk
|
||||||
.sorted()
|
.sorted()
|
||||||
.map { Page(file = it) }
|
.map { pageFromFileName(it) }
|
||||||
.toMutableList()
|
.toMutableList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun indexOfPage(id: String): Int =
|
private fun indexOfPage(id: String): Int =
|
||||||
pages.indexOfFirst { it.file == id }
|
pages.indexOfFirst { it.id == id }
|
||||||
|
|
||||||
private fun loadMetadata(): DocumentMetadata? =
|
private fun loadMetadata(): List<PageV2> {
|
||||||
if (metadataFile.exists()) {
|
val json = metadataFile.readText()
|
||||||
runCatching {
|
|
||||||
Json.decodeFromString<DocumentMetadata>(metadataFile.readText())
|
val jsonElement = Json.parseToJsonElement(json)
|
||||||
}.getOrNull()
|
val version = jsonElement.jsonObject["version"]?.jsonPrimitive?.int ?: 1
|
||||||
} else null
|
|
||||||
|
return when (version) {
|
||||||
|
1 -> migrateFromV1(Json.decodeFromJsonElement<DocumentMetadataV1>(jsonElement))
|
||||||
|
2 -> Json.decodeFromJsonElement<DocumentMetadataV2>(jsonElement).pages
|
||||||
|
else -> error("Unsupported metadata version: $version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrateFromV1(meta: DocumentMetadataV1): MutableList<PageV2> {
|
||||||
|
return meta.pages.map {
|
||||||
|
old -> pageFromFileName(old.file)
|
||||||
|
}.toMutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pageFromFileName(fileName: String): PageV2 {
|
||||||
|
val fileName = fileName.removeSuffix(".jpg")
|
||||||
|
val dashIndex = fileName.lastIndexOf('-')
|
||||||
|
val rotation = if (dashIndex >= 0)
|
||||||
|
fileName.substring(dashIndex + 1).toInt()
|
||||||
|
else
|
||||||
|
0
|
||||||
|
val id = if (dashIndex >= 0) fileName.substring(0, dashIndex) else fileName
|
||||||
|
return PageV2(id, manualRotationDegrees = rotation)
|
||||||
|
}
|
||||||
|
|
||||||
private fun saveMetadata() {
|
private fun saveMetadata() {
|
||||||
val metadata = DocumentMetadata(version = 1, pages = pages)
|
val metadata = DocumentMetadataV2(pages = pages)
|
||||||
metadataFile.writeText(Json.encodeToString(metadata))
|
metadataFile.writeText(json.encodeToString(metadata))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun imageIds(): PersistentList<String> =
|
fun pages(): List<ScanPage> =
|
||||||
pages.map { it.file }.toPersistentList()
|
pages.map {
|
||||||
|
ScanPage(it.id, it.toMetadata())
|
||||||
fun getPageMetadata(id: String): PageMetadata? {
|
|
||||||
val index = indexOfPage(id)
|
|
||||||
if (index < 0) return null
|
|
||||||
return pages[index].toMetadata()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO time complexity should be constant
|
||||||
|
private fun page(id: String): PageV2? = pages.find { p -> p.id == id }
|
||||||
|
|
||||||
fun add(pageBytes: ByteArray, sourceBytes: ByteArray, metadata: PageMetadata) {
|
fun add(pageBytes: ByteArray, sourceBytes: ByteArray, metadata: PageMetadata) {
|
||||||
val fileName = "${System.currentTimeMillis()}.jpg"
|
val id = "${System.currentTimeMillis()}"
|
||||||
|
val fileName = "$id.jpg"
|
||||||
val file = File(scanDir, fileName)
|
val file = File(scanDir, fileName)
|
||||||
file.writeBytes(pageBytes)
|
file.writeBytes(pageBytes)
|
||||||
writeThumbnail(file)
|
writeThumbnail(file)
|
||||||
File(sourceDir, fileName).writeBytes(sourceBytes)
|
File(sourceDir, fileName).writeBytes(sourceBytes)
|
||||||
pages.add(
|
pages.add(
|
||||||
Page(
|
PageV2(
|
||||||
file = fileName,
|
id = id,
|
||||||
quad = metadata.normalizedQuad.toSerializable(),
|
quad = metadata.normalizedQuad.toSerializable(),
|
||||||
rotationDegrees = metadata.rotationDegrees,
|
baseRotationDegrees = metadata.baseRotation.degrees,
|
||||||
|
manualRotationDegrees = metadata.manualRotation.degrees,
|
||||||
isColored = metadata.isColored
|
isColored = metadata.isColored
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
saveMetadata()
|
saveMetadata()
|
||||||
}
|
}
|
||||||
|
|
||||||
val idRegex = Regex("([0-9]+)(-(90|180|270))?\\.jpg")
|
private fun workFileName(key: PageViewKey): String =
|
||||||
|
workFileName(key.pageId, key.rotation)
|
||||||
|
|
||||||
|
private fun workFileName(pageId: String, manualRotation: Rotation): String =
|
||||||
|
workFileName(pageId, manualRotation.degrees)
|
||||||
|
|
||||||
|
private fun workFileName(pageId: String, manualRotationDegrees: Int): String =
|
||||||
|
when (manualRotationDegrees) {
|
||||||
|
0 -> "$pageId.jpg"
|
||||||
|
else -> "$pageId-${manualRotationDegrees}.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PageV2.workFileName() = workFileName(id, manualRotationDegrees)
|
||||||
|
|
||||||
fun rotate(id: String, clockwise: Boolean) {
|
fun rotate(id: String, clockwise: Boolean) {
|
||||||
val originalFile = File(scanDir, id)
|
|
||||||
if (!originalFile.exists()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
idRegex.matchEntire(id)?.let {
|
|
||||||
val baseId = it.groupValues[1]
|
|
||||||
val degrees = it.groupValues[3].ifEmpty { "0" }.toInt()
|
|
||||||
val targetDegrees = (degrees + (if (clockwise) 90 else 270)) % 360
|
|
||||||
val rotatedId = if (targetDegrees == 0) "$baseId.jpg" else "$baseId-$targetDegrees.jpg"
|
|
||||||
val rotatedFile = File(scanDir, rotatedId)
|
|
||||||
transformations.rotate(originalFile, rotatedFile, clockwise)
|
|
||||||
if (rotatedFile.exists()) {
|
|
||||||
val index = indexOfPage(id)
|
val index = indexOfPage(id)
|
||||||
if (index >= 0) {
|
if (index < 0)
|
||||||
val oldPage = pages[index]
|
return
|
||||||
pages[index] = oldPage.copy(file = rotatedId)
|
val page = pages[index]
|
||||||
|
|
||||||
|
val delta = if (clockwise) Rotation.R90 else Rotation.R270
|
||||||
|
val currentManualRotation = Rotation.fromDegrees(page.manualRotationDegrees)
|
||||||
|
val newManualRotation = currentManualRotation.add(delta)
|
||||||
|
if (newManualRotation == currentManualRotation) {
|
||||||
|
return // no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
val targetFileName = workFileName(page.id, newManualRotation)
|
||||||
|
val outputFile = File(scanDir, targetFileName)
|
||||||
|
if (!outputFile.exists()) {
|
||||||
|
val inputFile = File(scanDir, page.workFileName())
|
||||||
|
if (!inputFile.exists())
|
||||||
|
return
|
||||||
|
transformations.rotate(inputFile, outputFile, clockwise)
|
||||||
|
}
|
||||||
|
|
||||||
|
pages[index] = page.copy(
|
||||||
|
manualRotationDegrees = newManualRotation.degrees,
|
||||||
|
)
|
||||||
saveMetadata()
|
saveMetadata()
|
||||||
}
|
}
|
||||||
delete(id)
|
|
||||||
}
|
fun jpegBytes(key: PageViewKey): ByteArray? {
|
||||||
}
|
val file = File(scanDir, workFileName(key))
|
||||||
|
return (if (file.exists()) file else null)?.readBytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getContent(id: String): ByteArray? {
|
fun jpegBytes(id: String): ByteArray? {
|
||||||
return getFileFor(id)?.readBytes()
|
val page = page(id)
|
||||||
|
if (page == null) return null
|
||||||
|
val file = File(scanDir, page.workFileName())
|
||||||
|
return (if (file.exists()) file else null)?.readBytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFileFor(id: String): File? {
|
fun sourceJpegBytes(id: String): ByteArray? {
|
||||||
val file = File(scanDir, id)
|
val file = getSourceFile(id)
|
||||||
return if (file.exists()) file else null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSourceFor(id: String): ByteArray? {
|
|
||||||
val file = File(sourceDir, id)
|
|
||||||
return if (file.exists()) file.readBytes() else null
|
return if (file.exists()) file.readBytes() else null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getThumbnail(id: String): ByteArray? {
|
private fun getSourceFile(id: String): File {
|
||||||
val thumbFile = getThumbnailFile(id)
|
return File(sourceDir, "$id.jpg")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getThumbnail(key: PageViewKey): ByteArray? {
|
||||||
|
val thumbFile = getThumbnailFile(key)
|
||||||
|
if (thumbFile == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
if (!thumbFile.exists()) {
|
if (!thumbFile.exists()) {
|
||||||
val originalFile = File(scanDir, id)
|
val workFile = File(scanDir, workFileName(key))
|
||||||
if (!originalFile.exists()) return null
|
if (!workFile.exists()) return null
|
||||||
writeThumbnail(originalFile)
|
writeThumbnail(workFile)
|
||||||
}
|
}
|
||||||
return if (thumbFile.exists()) thumbFile.readBytes() else null
|
return if (thumbFile.exists()) thumbFile.readBytes() else null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun writeThumbnail(originalFile: File) {
|
private fun writeThumbnail(originalFile: File) {
|
||||||
val thumbFile = getThumbnailFile(originalFile.name)
|
val thumbFile = File(thumbnailDir, originalFile.name)
|
||||||
transformations.resize(originalFile, thumbFile, thumbnailSizePx)
|
transformations.resize(originalFile, thumbFile, thumbnailSizePx)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getThumbnailFile(id: String): File = File(thumbnailDir, id)
|
private fun getThumbnailFile(key: PageViewKey): File? {
|
||||||
|
return File(thumbnailDir, workFileName(key))
|
||||||
|
}
|
||||||
|
|
||||||
fun movePage(id: String, newIndex: Int) {
|
fun movePage(id: String, newIndex: Int) {
|
||||||
val index = indexOfPage(id)
|
val index = indexOfPage(id)
|
||||||
@@ -179,15 +243,24 @@ class ImageRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun delete(id: String) {
|
fun delete(id: String) {
|
||||||
File(scanDir, id).delete()
|
val index = indexOfPage(id)
|
||||||
File(sourceDir, id).delete()
|
if (index < 0)
|
||||||
getThumbnailFile(id).delete()
|
return
|
||||||
pages.removeAll { it.file == id }
|
pages.removeAt(index)
|
||||||
saveMetadata()
|
saveMetadata()
|
||||||
|
|
||||||
|
getSourceFile(id).delete()
|
||||||
|
scanDir.listFiles()
|
||||||
|
?.filter { it.name.startsWith("${id}.") || it.name.startsWith("$id-") }
|
||||||
|
?.forEach { it.delete() }
|
||||||
|
thumbnailDir.listFiles()
|
||||||
|
?.filter { it.name.startsWith("${id}.") || it.name.startsWith("$id-") }
|
||||||
|
?.forEach { it.delete() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
pages.clear()
|
pages.clear()
|
||||||
|
saveMetadata() // "empty" json file
|
||||||
thumbnailDir.listFiles()?.forEach {
|
thumbnailDir.listFiles()?.forEach {
|
||||||
file -> file.delete()
|
file -> file.delete()
|
||||||
}
|
}
|
||||||
@@ -197,7 +270,6 @@ class ImageRepository(
|
|||||||
sourceDir.listFiles()?.forEach {
|
sourceDir.listFiles()?.forEach {
|
||||||
file -> file.delete()
|
file -> file.delete()
|
||||||
}
|
}
|
||||||
saveMetadata() // "empty" json file
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +289,14 @@ fun NormalizedQuad.toQuad(): Quad =
|
|||||||
Point(bottomLeft.x, bottomLeft.y)
|
Point(bottomLeft.x, bottomLeft.y)
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Page.toMetadata(): PageMetadata? {
|
fun PageV2.toMetadata(): PageMetadata? {
|
||||||
|
return runCatching {
|
||||||
if (quad == null || isColored == null) return null
|
if (quad == null || isColored == null) return null
|
||||||
return PageMetadata(quad.toQuad(), rotationDegrees, isColored)
|
PageMetadata(
|
||||||
|
quad.toQuad(),
|
||||||
|
Rotation.fromDegrees(baseRotationDegrees),
|
||||||
|
Rotation.fromDegrees(manualRotationDegrees),
|
||||||
|
isColored
|
||||||
|
)
|
||||||
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,26 +28,27 @@ fun jpegsForExport(
|
|||||||
exportQuality: ExportQuality
|
exportQuality: ExportQuality
|
||||||
): Sequence<ByteArray> {
|
): Sequence<ByteArray> {
|
||||||
|
|
||||||
val imageIds = imageRepository.imageIds()
|
val pages = imageRepository.pages().asSequence()
|
||||||
val baseJpegs = imageIds.asSequence().mapNotNull { id -> imageRepository.getContent(id) }
|
|
||||||
return when (exportQuality) {
|
return when (exportQuality) {
|
||||||
ExportQuality.BALANCED -> baseJpegs
|
ExportQuality.BALANCED -> pages.mapNotNull { imageRepository.jpegBytes(it.id) }
|
||||||
ExportQuality.LOW -> baseJpegs.mapNotNull { jpeg ->
|
|
||||||
|
ExportQuality.LOW -> pages.mapNotNull { page ->
|
||||||
|
imageRepository.jpegBytes(page.id)?.let { jpeg ->
|
||||||
resizeJpegBytesForMaxPixels(
|
resizeJpegBytesForMaxPixels(
|
||||||
jpegBytes = jpeg,
|
jpegBytes = jpeg,
|
||||||
maxPixels = exportQuality.maxPixels.toDouble(),
|
maxPixels = exportQuality.maxPixels.toDouble(),
|
||||||
jpegQuality = exportQuality.jpegQuality
|
jpegQuality = exportQuality.jpegQuality
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ExportQuality.HIGH -> {
|
|
||||||
imageIds.asSequence().mapNotNull { id ->
|
|
||||||
val sourceJpegBytes = imageRepository.getSourceFor(id)
|
|
||||||
val pageMetadata = imageRepository.getPageMetadata(id)
|
|
||||||
if (sourceJpegBytes != null && pageMetadata != null)
|
|
||||||
prepareJpegForHigh(sourceJpegBytes, pageMetadata, ExportQuality.HIGH)
|
|
||||||
else
|
|
||||||
imageRepository.getContent(id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExportQuality.HIGH -> pages.mapNotNull { page ->
|
||||||
|
val sourceJpegBytes = imageRepository.sourceJpegBytes(page.id)
|
||||||
|
val pageMetadata = page.metadata
|
||||||
|
if (sourceJpegBytes != null && pageMetadata != null)
|
||||||
|
prepareJpegForHigh(sourceJpegBytes, pageMetadata, exportQuality)
|
||||||
|
else
|
||||||
|
imageRepository.jpegBytes(page.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,7 +84,7 @@ fun prepareJpegForHigh(
|
|||||||
val page = extractDocument(
|
val page = extractDocument(
|
||||||
decoded,
|
decoded,
|
||||||
quad,
|
quad,
|
||||||
pageMetadata.rotationDegrees,
|
pageMetadata.baseRotation.add(pageMetadata.manualRotation).degrees,
|
||||||
pageMetadata.isColored,
|
pageMetadata.isColored,
|
||||||
exportQuality.maxPixels)
|
exportQuality.maxPixels)
|
||||||
val outJpegBytes = encodeJpeg(page, exportQuality.jpegQuality)
|
val outJpegBytes = encodeJpeg(page, exportQuality.jpegQuality)
|
||||||
|
|||||||
@@ -18,6 +18,34 @@ import org.fairscan.imageprocessing.Quad
|
|||||||
|
|
||||||
data class PageMetadata(
|
data class PageMetadata(
|
||||||
val normalizedQuad: Quad,
|
val normalizedQuad: Quad,
|
||||||
val rotationDegrees: Int,
|
val baseRotation: Rotation,
|
||||||
|
val manualRotation: Rotation,
|
||||||
val isColored: Boolean,
|
val isColored: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class ScanPage(
|
||||||
|
val id: String,
|
||||||
|
val metadata: PageMetadata?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PageViewKey(
|
||||||
|
val pageId: String,
|
||||||
|
val rotation: Rotation,
|
||||||
|
) {
|
||||||
|
val saveKey: String get() = "$pageId-${rotation.degrees}"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Rotation(val degrees: Int) {
|
||||||
|
R0(0),
|
||||||
|
R90(90),
|
||||||
|
R180(180),
|
||||||
|
R270(270);
|
||||||
|
|
||||||
|
fun add(other: Rotation): Rotation =
|
||||||
|
fromDegrees((degrees + other.degrees) % 360)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromDegrees(deg: Int): Rotation =
|
||||||
|
entries.first { it.degrees == ((deg % 360 + 360) % 360) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,9 @@ import android.content.Context
|
|||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
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.DocumentUiModel
|
||||||
|
|
||||||
fun dummyNavigation(): Navigation {
|
fun dummyNavigation(): Navigation {
|
||||||
@@ -29,10 +32,11 @@ fun fakeDocument(): DocumentUiModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun fakeDocument(pageIds: ImmutableList<String>, context: Context): DocumentUiModel {
|
fun fakeDocument(pageIds: ImmutableList<String>, context: Context): DocumentUiModel {
|
||||||
val loader = { id:String ->
|
val loader = { key: PageViewKey ->
|
||||||
context.assets.open(id).use { input ->
|
context.assets.open("${key.pageId}.jpg").use { input ->
|
||||||
BitmapFactory.decodeStream(input)
|
BitmapFactory.decodeStream(input)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return DocumentUiModel(pageIds, loader, loader)
|
val pageKeys = pageIds.map { PageViewKey(it, Rotation.R0) }.toImmutableList()
|
||||||
|
return DocumentUiModel(pageKeys, loader, loader)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ fun CommonPageList(
|
|||||||
state.onPageReorder(from.key as String, to.index)
|
state.onPageReorder(from.key as String, to.index)
|
||||||
}
|
}
|
||||||
val content: LazyListScope.() -> Unit = {
|
val content: LazyListScope.() -> Unit = {
|
||||||
itemsIndexed(state.document.pageIds, key = { _, item -> item}) { index, item ->
|
itemsIndexed(state.document.pageKeys, key = { _, item -> item.saveKey}) { index, item ->
|
||||||
ReorderableItem(reorderableLazyListState, key = item) { isDragging ->
|
ReorderableItem(reorderableLazyListState, key = item.saveKey) { isDragging ->
|
||||||
val borderColor =
|
val borderColor =
|
||||||
if (isDragging) MaterialTheme.colorScheme.primary else Color.Transparent
|
if (isDragging) MaterialTheme.colorScheme.primary else Color.Transparent
|
||||||
val modifier = Modifier
|
val modifier = Modifier
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ import org.fairscan.app.MainViewModel
|
|||||||
import org.fairscan.app.R
|
import org.fairscan.app.R
|
||||||
import org.fairscan.app.domain.CapturedPage
|
import org.fairscan.app.domain.CapturedPage
|
||||||
import org.fairscan.app.domain.PageMetadata
|
import org.fairscan.app.domain.PageMetadata
|
||||||
|
import org.fairscan.app.domain.Rotation.R0
|
||||||
import org.fairscan.app.ui.Navigation
|
import org.fairscan.app.ui.Navigation
|
||||||
import org.fairscan.app.ui.Screen
|
import org.fairscan.app.ui.Screen
|
||||||
import org.fairscan.app.ui.components.CameraPermissionState
|
import org.fairscan.app.ui.components.CameraPermissionState
|
||||||
@@ -471,7 +472,7 @@ fun CameraScreenPreviewWithProcessedImage() {
|
|||||||
CapturedPage(
|
CapturedPage(
|
||||||
debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
|
debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
|
||||||
debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
|
debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
|
||||||
PageMetadata(quad, 0, false))))
|
PageMetadata(quad, R0, R0, false))))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true, widthDp = 640, heightDp = 320)
|
@Preview(showBackground = true, widthDp = 640, heightDp = 320)
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import org.fairscan.app.AppContainer
|
|||||||
import org.fairscan.app.domain.CapturedPage
|
import org.fairscan.app.domain.CapturedPage
|
||||||
import org.fairscan.app.domain.ExportQuality
|
import org.fairscan.app.domain.ExportQuality
|
||||||
import org.fairscan.app.domain.PageMetadata
|
import org.fairscan.app.domain.PageMetadata
|
||||||
|
import org.fairscan.app.domain.Rotation
|
||||||
import org.fairscan.imageprocessing.Mask
|
import org.fairscan.imageprocessing.Mask
|
||||||
import org.fairscan.imageprocessing.Quad
|
import org.fairscan.imageprocessing.Quad
|
||||||
import org.fairscan.imageprocessing.detectDocumentQuad
|
import org.fairscan.imageprocessing.detectDocumentQuad
|
||||||
@@ -206,7 +207,8 @@ fun extractDocumentFromBitmap(
|
|||||||
val outBitmap = toBitmap(outBgr)
|
val outBitmap = toBitmap(outBgr)
|
||||||
outBgr.release()
|
outBgr.release()
|
||||||
val normalizedQuad = quad.scaledTo(source.width, source.height, 1, 1)
|
val normalizedQuad = quad.scaledTo(source.width, source.height, 1, 1)
|
||||||
val metadata = PageMetadata(normalizedQuad, rotationDegrees, isColored)
|
val baseRotation = Rotation.fromDegrees(rotationDegrees)
|
||||||
|
val metadata = PageMetadata(normalizedQuad, baseRotation, Rotation.R0, isColored)
|
||||||
return CapturedPage(outBitmap, source, metadata)
|
return CapturedPage(outBitmap, source, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,28 +16,29 @@ 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 org.fairscan.app.domain.PageViewKey
|
||||||
|
|
||||||
data class DocumentUiModel(
|
data class DocumentUiModel(
|
||||||
val pageIds: ImmutableList<String>,
|
val pageKeys: ImmutableList<PageViewKey>,
|
||||||
private val imageLoader: (String) -> Bitmap?,
|
private val imageLoader: (PageViewKey) -> Bitmap?,
|
||||||
private val thumbnailLoader: (String) -> Bitmap?
|
private val thumbnailLoader: (PageViewKey) -> Bitmap?
|
||||||
) {
|
) {
|
||||||
fun pageCount(): Int {
|
fun pageCount(): Int {
|
||||||
return pageIds.size
|
return pageKeys.size
|
||||||
}
|
}
|
||||||
fun pageId(index: Int): String {
|
fun pageId(index: Int): String {
|
||||||
return pageIds[index]
|
return pageKeys[index].pageId
|
||||||
}
|
}
|
||||||
fun isEmpty(): Boolean {
|
fun isEmpty(): Boolean {
|
||||||
return pageIds.isEmpty()
|
return pageKeys.isEmpty()
|
||||||
}
|
}
|
||||||
fun lastIndex(): Int {
|
fun lastIndex(): Int {
|
||||||
return pageIds.lastIndex
|
return pageKeys.lastIndex
|
||||||
}
|
}
|
||||||
fun load(index: Int): Bitmap? {
|
fun load(index: Int): Bitmap? {
|
||||||
return imageLoader(pageIds[index])
|
return imageLoader(pageKeys[index])
|
||||||
}
|
}
|
||||||
fun loadThumbnail(index: Int): Bitmap? {
|
fun loadThumbnail(index: Int): Bitmap? {
|
||||||
return thumbnailLoader(pageIds[index])
|
return thumbnailLoader(pageKeys[index])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,8 +14,15 @@
|
|||||||
*/
|
*/
|
||||||
package org.fairscan.app.data
|
package org.fairscan.app.data
|
||||||
|
|
||||||
|
import kotlinx.collections.immutable.PersistentList
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.fairscan.app.domain.PageMetadata
|
import org.fairscan.app.domain.PageMetadata
|
||||||
|
import org.fairscan.app.domain.PageViewKey
|
||||||
|
import org.fairscan.app.domain.Rotation.R0
|
||||||
|
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.Point
|
import org.fairscan.imageprocessing.Point
|
||||||
import org.fairscan.imageprocessing.Quad
|
import org.fairscan.imageprocessing.Quad
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
@@ -31,7 +38,7 @@ class ImageRepositoryTest {
|
|||||||
private var _filesDir: File? = null
|
private var _filesDir: File? = null
|
||||||
|
|
||||||
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, 90, true)
|
val metadata1 = PageMetadata(quad1, R90, R0, true)
|
||||||
|
|
||||||
fun getFilesDir(): File {
|
fun getFilesDir(): File {
|
||||||
if (_filesDir == null) {
|
if (_filesDir == null) {
|
||||||
@@ -60,14 +67,17 @@ class ImageRepositoryTest {
|
|||||||
repo.add(bytes, byteArrayOf(51), metadata1)
|
repo.add(bytes, byteArrayOf(51), metadata1)
|
||||||
assertThat(repo.imageIds()).hasSize(1)
|
assertThat(repo.imageIds()).hasSize(1)
|
||||||
val id = repo.imageIds()[0]
|
val id = repo.imageIds()[0]
|
||||||
assertThat(repo.getContent(id)).isEqualTo(bytes)
|
val key = PageViewKey(id, R0)
|
||||||
assertThat(repo.getThumbnail(id)).isEqualTo(byteArrayOf(101))
|
assertThat(repo.jpegBytes(key)).isEqualTo(bytes)
|
||||||
|
assertThat(repo.getThumbnail(key)).isEqualTo(byteArrayOf(101))
|
||||||
|
|
||||||
assertThat(repo().getPageMetadata("x")).isNull()
|
val page = repo.pages().first()
|
||||||
val metadata = repo.getPageMetadata(id)
|
assertThat(page.id).isEqualTo(id)
|
||||||
|
val metadata = page.metadata
|
||||||
assertThat(metadata).isNotNull()
|
assertThat(metadata).isNotNull()
|
||||||
assertThat(metadata!!.normalizedQuad).isEqualTo(quad1)
|
assertThat(metadata!!.normalizedQuad).isEqualTo(quad1)
|
||||||
assertThat(metadata.rotationDegrees).isEqualTo(metadata1.rotationDegrees)
|
assertThat(metadata.baseRotation).isEqualTo(metadata1.baseRotation)
|
||||||
|
assertThat(metadata.manualRotation).isEqualTo(metadata1.manualRotation)
|
||||||
assertThat(metadata.isColored).isEqualTo(metadata1.isColored)
|
assertThat(metadata.isColored).isEqualTo(metadata1.isColored)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +108,17 @@ class ImageRepositoryTest {
|
|||||||
fun `should find existing files at initialization with no json`() {
|
fun `should find existing files at initialization with no json`() {
|
||||||
scanDir().mkdirs()
|
scanDir().mkdirs()
|
||||||
File(scanDir(), "1.jpg").writeBytes(byteArrayOf(101, 102, 103))
|
File(scanDir(), "1.jpg").writeBytes(byteArrayOf(101, 102, 103))
|
||||||
assertThat(repo().imageIds()).containsExactly("1.jpg")
|
assertThat(repo().imageIds()).containsExactly("1")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should find existing files at initialization with no json and with rotation`() {
|
||||||
|
scanDir().mkdirs()
|
||||||
|
val bytes = byteArrayOf(101, 102, 103)
|
||||||
|
File(scanDir(), "1-90.jpg").writeBytes(bytes)
|
||||||
|
val repo = repo()
|
||||||
|
assertThat(repo.imageIds()).containsExactly("1")
|
||||||
|
assertThat(repo.jpegBytes("1")).isEqualTo(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -107,14 +127,14 @@ class ImageRepositoryTest {
|
|||||||
val json = """{"pages":[{"file":"1.jpg"}, {"file":"2.jpg"}]}"""
|
val json = """{"pages":[{"file":"1.jpg"}, {"file":"2.jpg"}]}"""
|
||||||
File(scanDir(), "document.json").writeText(json)
|
File(scanDir(), "document.json").writeText(json)
|
||||||
File(scanDir(), "2.jpg").writeBytes(byteArrayOf(101, 102, 103))
|
File(scanDir(), "2.jpg").writeBytes(byteArrayOf(101, 102, 103))
|
||||||
assertThat(repo().imageIds()).containsExactly("2.jpg")
|
assertThat(repo().imageIds()).containsExactly("2")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should return null on invalid id`() {
|
fun `should return null on invalid id`() {
|
||||||
val repo = repo()
|
val repo = repo()
|
||||||
assertThat(repo.imageIds()).isEmpty()
|
assertThat(repo.imageIds()).isEmpty()
|
||||||
assertThat(repo.getContent("x")).isNull()
|
assertThat(repo.jpegBytes("x")).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -136,28 +156,19 @@ class ImageRepositoryTest {
|
|||||||
fun rotate() {
|
fun rotate() {
|
||||||
val repo = repo()
|
val repo = repo()
|
||||||
repo.add(byteArrayOf(101, 102, 103), byteArrayOf(51), metadata1)
|
repo.add(byteArrayOf(101, 102, 103), byteArrayOf(51), metadata1)
|
||||||
val id0 = repo.imageIds().last()
|
assertThat(metadata1.manualRotation).isEqualTo(R0)
|
||||||
val baseId = id0.substring(0, id0.length - 4)
|
assertThat(repo.pages().last().metadata).isEqualTo(metadata1)
|
||||||
|
val id = repo.pages().last().id
|
||||||
repo.rotate(id0, true)
|
repo.rotate(id, true)
|
||||||
val id1 = repo.imageIds().last()
|
assertThat(repo.pages().last().metadata).isEqualTo(metadata1.copy(manualRotation = R90))
|
||||||
assertThat(id1).isEqualTo("$baseId-90.jpg")
|
repo.rotate(id, true)
|
||||||
|
assertThat(repo.pages().last().metadata).isEqualTo(metadata1.copy(manualRotation = R180))
|
||||||
repo.rotate(id1, true)
|
repo.rotate(id, true)
|
||||||
val id2 = repo.imageIds().last()
|
assertThat(repo.pages().last().metadata).isEqualTo(metadata1.copy(manualRotation = R270))
|
||||||
assertThat(id2).isEqualTo("$baseId-180.jpg")
|
repo.rotate(id, true)
|
||||||
|
assertThat(repo.pages().last().metadata).isEqualTo(metadata1.copy(manualRotation = R0))
|
||||||
repo.rotate(id2, true)
|
repo.rotate(id, false)
|
||||||
val id3 = repo.imageIds().last()
|
assertThat(repo.pages().last().metadata).isEqualTo(metadata1.copy(manualRotation = R270))
|
||||||
assertThat(id3).isEqualTo("$baseId-270.jpg")
|
|
||||||
|
|
||||||
repo.rotate(id3, true)
|
|
||||||
val id4 = repo.imageIds().last()
|
|
||||||
assertThat(id4).isEqualTo("$baseId.jpg")
|
|
||||||
|
|
||||||
repo.rotate(id4, false)
|
|
||||||
val id5 = repo.imageIds().last()
|
|
||||||
assertThat(id5).isEqualTo("$baseId-270.jpg")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -179,14 +190,16 @@ class ImageRepositoryTest {
|
|||||||
fun metadata() {
|
fun metadata() {
|
||||||
val quad = quad1.toSerializable()
|
val quad = quad1.toSerializable()
|
||||||
|
|
||||||
assertThat(Page("f1", null, 0, true).toMetadata()).isNull()
|
assertThat(PageV2("1", 0, 0, null,true).toMetadata()).isNull()
|
||||||
assertThat(Page("f1", quad, 0, null).toMetadata()).isNull()
|
assertThat(PageV2("1", 0, 0, quad, null).toMetadata()).isNull()
|
||||||
|
|
||||||
listOf(true, false).forEach { isColored ->
|
listOf(true, false).forEach { isColored ->
|
||||||
val metadata = Page("f1", quad, 0, isColored).toMetadata()
|
val metadata = PageV2("1", 0, 0, quad, isColored).toMetadata()
|
||||||
assertThat(metadata).isNotNull()
|
assertThat(metadata).isNotNull()
|
||||||
assertThat(metadata!!.isColored).isEqualTo(isColored)
|
assertThat(metadata!!.isColored).isEqualTo(isColored)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assertThat(PageV2("1", 42, 0, quad, true).toMetadata()).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scanDir(): File = File(getFilesDir(), SCAN_DIR_NAME)
|
private fun scanDir(): File = File(getFilesDir(), SCAN_DIR_NAME)
|
||||||
@@ -194,4 +207,7 @@ class ImageRepositoryTest {
|
|||||||
|
|
||||||
private fun jpegFiles(dir: File): Array<out File?>?
|
private fun jpegFiles(dir: File): Array<out File?>?
|
||||||
= dir.listFiles { f -> f.name.endsWith(".jpg") }
|
= dir.listFiles { f -> f.name.endsWith(".jpg") }
|
||||||
|
|
||||||
|
fun ImageRepository.imageIds(): PersistentList<String> =
|
||||||
|
pages().map { it.id }.toPersistentList()
|
||||||
}
|
}
|
||||||
|
|||||||
47
app/src/test/java/org/fairscan/app/domain/RotationTest.kt
Normal file
47
app/src/test/java/org/fairscan/app/domain/RotationTest.kt
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.fairscan.app.domain
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
|
import org.fairscan.app.domain.Rotation.Companion.fromDegrees
|
||||||
|
import org.fairscan.app.domain.Rotation.R0
|
||||||
|
import org.fairscan.app.domain.Rotation.R180
|
||||||
|
import org.fairscan.app.domain.Rotation.R270
|
||||||
|
import org.fairscan.app.domain.Rotation.R90
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class RotationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fromDegreesFunction() {
|
||||||
|
assertThat(fromDegrees(0)).isEqualTo(R0)
|
||||||
|
assertThat(fromDegrees(90)).isEqualTo(R90)
|
||||||
|
assertThat(fromDegrees(180)).isEqualTo(R180)
|
||||||
|
assertThat(fromDegrees(270)).isEqualTo(R270)
|
||||||
|
assertThat(fromDegrees(360)).isEqualTo(R0)
|
||||||
|
assertThat(fromDegrees(-90)).isEqualTo(R270)
|
||||||
|
assertThatThrownBy { fromDegrees(30) }.isInstanceOf(NoSuchElementException::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun add() {
|
||||||
|
assertThat(R0.add(R90)).isEqualTo(R90)
|
||||||
|
assertThat(R90.add(R90)).isEqualTo(R180)
|
||||||
|
assertThat(R90.add(R180)).isEqualTo(R270)
|
||||||
|
assertThat(R180.add(R180)).isEqualTo(R0)
|
||||||
|
assertThat(R180.add(R270)).isEqualTo(R90)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user