Revamp ImageRepository
This commit is contained in:
@@ -19,6 +19,7 @@ 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.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -28,6 +29,8 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.fairscan.app.data.ImageRepository
|
||||
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.Screen
|
||||
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 }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, _navigationState.value.current)
|
||||
|
||||
private val _pageIds = MutableStateFlow(imageRepository.imageIds())
|
||||
private val _pages = MutableStateFlow(imageRepository.pages())
|
||||
val documentUiModel: StateFlow<DocumentUiModel> =
|
||||
_pageIds.map { ids ->
|
||||
_pages.map { pages ->
|
||||
DocumentUiModel(
|
||||
pageIds = ids,
|
||||
pageKeys = pages.map { p ->
|
||||
PageViewKey(p.id, p.metadata?.manualRotation?: Rotation.R0)
|
||||
}.toImmutableList(),
|
||||
imageLoader = ::getBitmap,
|
||||
thumbnailLoader = ::getThumbnail,
|
||||
)
|
||||
@@ -64,38 +69,38 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
||||
fun rotateImage(id: String, clockwise: Boolean) {
|
||||
viewModelScope.launch {
|
||||
imageRepository.rotate(id, clockwise)
|
||||
_pageIds.value = imageRepository.imageIds()
|
||||
_pages.value = imageRepository.pages()
|
||||
}
|
||||
}
|
||||
|
||||
fun movePage(id: String, newIndex: Int) {
|
||||
viewModelScope.launch {
|
||||
imageRepository.movePage(id, newIndex)
|
||||
_pageIds.value = imageRepository.imageIds()
|
||||
_pages.value = imageRepository.pages()
|
||||
}
|
||||
}
|
||||
|
||||
fun deletePage(id: String) {
|
||||
viewModelScope.launch {
|
||||
imageRepository.delete(id)
|
||||
_pageIds.value = imageRepository.imageIds()
|
||||
_pages.value = imageRepository.pages()
|
||||
}
|
||||
}
|
||||
|
||||
fun startNewDocument() {
|
||||
_pageIds.value = persistentListOf()
|
||||
_pages.value = persistentListOf()
|
||||
viewModelScope.launch {
|
||||
imageRepository.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun getBitmap(id: String): Bitmap? {
|
||||
val bytes = imageRepository.getContent(id)
|
||||
fun getBitmap(key: PageViewKey): Bitmap? {
|
||||
val bytes = imageRepository.jpegBytes(key)
|
||||
return bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
||||
}
|
||||
|
||||
fun getThumbnail(id: String): Bitmap? {
|
||||
val bytes = imageRepository.getThumbnail(id)
|
||||
fun getThumbnail(key: PageViewKey): Bitmap? {
|
||||
val bytes = imageRepository.getThumbnail(key)
|
||||
return bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
||||
}
|
||||
|
||||
@@ -106,7 +111,7 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
||||
compressJpeg(capturedPage.source, 90),
|
||||
capturedPage.metadata,
|
||||
)
|
||||
_pageIds.value = imageRepository.imageIds()
|
||||
_pages.value = imageRepository.pages()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,16 +17,28 @@ package org.fairscan.app.data
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class DocumentMetadata(
|
||||
data class DocumentMetadataV1(
|
||||
val version: Int = 1,
|
||||
val pages: List<Page>
|
||||
val pages: List<PageV1>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Page(
|
||||
val file: String,
|
||||
data class PageV1(
|
||||
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 rotationDegrees: Int = 0,
|
||||
val isColored: Boolean? = null
|
||||
)
|
||||
|
||||
|
||||
@@ -14,10 +14,15 @@
|
||||
*/
|
||||
package org.fairscan.app.data
|
||||
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
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.PageViewKey
|
||||
import org.fairscan.app.domain.Rotation
|
||||
import org.fairscan.app.domain.ScanPage
|
||||
import org.fairscan.imageprocessing.Point
|
||||
import org.fairscan.imageprocessing.Quad
|
||||
import java.io.File
|
||||
@@ -46,127 +51,186 @@ class ImageRepository(
|
||||
|
||||
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()
|
||||
?.filter { it.extension == "jpg" }
|
||||
?.map { it.name }
|
||||
?.toSet()
|
||||
?: emptySet()
|
||||
|
||||
val metadataPages = loadMetadata()?.pages
|
||||
val metadataPages = if (metadataFile.exists()) {
|
||||
runCatching { loadMetadata() }.getOrNull()
|
||||
} else null
|
||||
|
||||
return when {
|
||||
metadataPages != null ->
|
||||
metadataPages
|
||||
.filter { it.file in filesOnDisk }
|
||||
.filter { it.workFileName() in filesOnDisk }
|
||||
.toMutableList()
|
||||
else ->
|
||||
filesOnDisk
|
||||
.sorted()
|
||||
.map { Page(file = it) }
|
||||
.map { pageFromFileName(it) }
|
||||
.toMutableList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun indexOfPage(id: String): Int =
|
||||
pages.indexOfFirst { it.file == id }
|
||||
pages.indexOfFirst { it.id == id }
|
||||
|
||||
private fun loadMetadata(): DocumentMetadata? =
|
||||
if (metadataFile.exists()) {
|
||||
runCatching {
|
||||
Json.decodeFromString<DocumentMetadata>(metadataFile.readText())
|
||||
}.getOrNull()
|
||||
} else null
|
||||
private fun loadMetadata(): List<PageV2> {
|
||||
val json = metadataFile.readText()
|
||||
|
||||
val jsonElement = Json.parseToJsonElement(json)
|
||||
val version = jsonElement.jsonObject["version"]?.jsonPrimitive?.int ?: 1
|
||||
|
||||
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() {
|
||||
val metadata = DocumentMetadata(version = 1, pages = pages)
|
||||
metadataFile.writeText(Json.encodeToString(metadata))
|
||||
val metadata = DocumentMetadataV2(pages = pages)
|
||||
metadataFile.writeText(json.encodeToString(metadata))
|
||||
}
|
||||
|
||||
fun imageIds(): PersistentList<String> =
|
||||
pages.map { it.file }.toPersistentList()
|
||||
fun pages(): List<ScanPage> =
|
||||
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) {
|
||||
val fileName = "${System.currentTimeMillis()}.jpg"
|
||||
val id = "${System.currentTimeMillis()}"
|
||||
val fileName = "$id.jpg"
|
||||
val file = File(scanDir, fileName)
|
||||
file.writeBytes(pageBytes)
|
||||
writeThumbnail(file)
|
||||
File(sourceDir, fileName).writeBytes(sourceBytes)
|
||||
pages.add(
|
||||
Page(
|
||||
file = fileName,
|
||||
PageV2(
|
||||
id = id,
|
||||
quad = metadata.normalizedQuad.toSerializable(),
|
||||
rotationDegrees = metadata.rotationDegrees,
|
||||
baseRotationDegrees = metadata.baseRotation.degrees,
|
||||
manualRotationDegrees = metadata.manualRotation.degrees,
|
||||
isColored = metadata.isColored
|
||||
)
|
||||
)
|
||||
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) {
|
||||
val originalFile = File(scanDir, id)
|
||||
if (!originalFile.exists()) {
|
||||
val index = indexOfPage(id)
|
||||
if (index < 0)
|
||||
return
|
||||
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
|
||||
}
|
||||
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)
|
||||
if (index >= 0) {
|
||||
val oldPage = pages[index]
|
||||
pages[index] = oldPage.copy(file = rotatedId)
|
||||
saveMetadata()
|
||||
}
|
||||
delete(id)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
fun getContent(id: String): ByteArray? {
|
||||
return getFileFor(id)?.readBytes()
|
||||
fun jpegBytes(key: PageViewKey): ByteArray? {
|
||||
val file = File(scanDir, workFileName(key))
|
||||
return (if (file.exists()) file else null)?.readBytes()
|
||||
}
|
||||
|
||||
fun getFileFor(id: String): File? {
|
||||
val file = File(scanDir, id)
|
||||
return if (file.exists()) file else null
|
||||
fun jpegBytes(id: String): ByteArray? {
|
||||
val page = page(id)
|
||||
if (page == null) return null
|
||||
val file = File(scanDir, page.workFileName())
|
||||
return (if (file.exists()) file else null)?.readBytes()
|
||||
}
|
||||
|
||||
fun getSourceFor(id: String): ByteArray? {
|
||||
val file = File(sourceDir, id)
|
||||
fun sourceJpegBytes(id: String): ByteArray? {
|
||||
val file = getSourceFile(id)
|
||||
return if (file.exists()) file.readBytes() else null
|
||||
}
|
||||
|
||||
fun getThumbnail(id: String): ByteArray? {
|
||||
val thumbFile = getThumbnailFile(id)
|
||||
private fun getSourceFile(id: String): File {
|
||||
return File(sourceDir, "$id.jpg")
|
||||
}
|
||||
|
||||
fun getThumbnail(key: PageViewKey): ByteArray? {
|
||||
val thumbFile = getThumbnailFile(key)
|
||||
if (thumbFile == null) {
|
||||
return null
|
||||
}
|
||||
if (!thumbFile.exists()) {
|
||||
val originalFile = File(scanDir, id)
|
||||
if (!originalFile.exists()) return null
|
||||
writeThumbnail(originalFile)
|
||||
val workFile = File(scanDir, workFileName(key))
|
||||
if (!workFile.exists()) return null
|
||||
writeThumbnail(workFile)
|
||||
}
|
||||
return if (thumbFile.exists()) thumbFile.readBytes() else null
|
||||
}
|
||||
|
||||
private fun writeThumbnail(originalFile: File) {
|
||||
val thumbFile = getThumbnailFile(originalFile.name)
|
||||
val thumbFile = File(thumbnailDir, originalFile.name)
|
||||
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) {
|
||||
val index = indexOfPage(id)
|
||||
@@ -179,15 +243,24 @@ class ImageRepository(
|
||||
}
|
||||
|
||||
fun delete(id: String) {
|
||||
File(scanDir, id).delete()
|
||||
File(sourceDir, id).delete()
|
||||
getThumbnailFile(id).delete()
|
||||
pages.removeAll { it.file == id }
|
||||
val index = indexOfPage(id)
|
||||
if (index < 0)
|
||||
return
|
||||
pages.removeAt(index)
|
||||
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() {
|
||||
pages.clear()
|
||||
saveMetadata() // "empty" json file
|
||||
thumbnailDir.listFiles()?.forEach {
|
||||
file -> file.delete()
|
||||
}
|
||||
@@ -197,7 +270,6 @@ class ImageRepository(
|
||||
sourceDir.listFiles()?.forEach {
|
||||
file -> file.delete()
|
||||
}
|
||||
saveMetadata() // "empty" json file
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,9 +287,16 @@ fun NormalizedQuad.toQuad(): Quad =
|
||||
Point(topRight.x, topRight.y),
|
||||
Point(bottomRight.x, bottomRight.y),
|
||||
Point(bottomLeft.x, bottomLeft.y)
|
||||
)
|
||||
)
|
||||
|
||||
fun Page.toMetadata(): PageMetadata? {
|
||||
if (quad == null || isColored == null) return null
|
||||
return PageMetadata(quad.toQuad(), rotationDegrees, isColored)
|
||||
fun PageV2.toMetadata(): PageMetadata? {
|
||||
return runCatching {
|
||||
if (quad == null || isColored == null) return null
|
||||
PageMetadata(
|
||||
quad.toQuad(),
|
||||
Rotation.fromDegrees(baseRotationDegrees),
|
||||
Rotation.fromDegrees(manualRotationDegrees),
|
||||
isColored
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
@@ -28,27 +28,28 @@ fun jpegsForExport(
|
||||
exportQuality: ExportQuality
|
||||
): Sequence<ByteArray> {
|
||||
|
||||
val imageIds = imageRepository.imageIds()
|
||||
val baseJpegs = imageIds.asSequence().mapNotNull { id -> imageRepository.getContent(id) }
|
||||
val pages = imageRepository.pages().asSequence()
|
||||
return when (exportQuality) {
|
||||
ExportQuality.BALANCED -> baseJpegs
|
||||
ExportQuality.LOW -> baseJpegs.mapNotNull { jpeg ->
|
||||
resizeJpegBytesForMaxPixels(
|
||||
jpegBytes = jpeg,
|
||||
maxPixels = exportQuality.maxPixels.toDouble(),
|
||||
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.BALANCED -> pages.mapNotNull { imageRepository.jpegBytes(it.id) }
|
||||
|
||||
ExportQuality.LOW -> pages.mapNotNull { page ->
|
||||
imageRepository.jpegBytes(page.id)?.let { jpeg ->
|
||||
resizeJpegBytesForMaxPixels(
|
||||
jpegBytes = jpeg,
|
||||
maxPixels = exportQuality.maxPixels.toDouble(),
|
||||
jpegQuality = exportQuality.jpegQuality
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
decoded,
|
||||
quad,
|
||||
pageMetadata.rotationDegrees,
|
||||
pageMetadata.baseRotation.add(pageMetadata.manualRotation).degrees,
|
||||
pageMetadata.isColored,
|
||||
exportQuality.maxPixels)
|
||||
val outJpegBytes = encodeJpeg(page, exportQuality.jpegQuality)
|
||||
|
||||
@@ -18,6 +18,34 @@ import org.fairscan.imageprocessing.Quad
|
||||
|
||||
data class PageMetadata(
|
||||
val normalizedQuad: Quad,
|
||||
val rotationDegrees: Int,
|
||||
val baseRotation: Rotation,
|
||||
val manualRotation: Rotation,
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
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
|
||||
|
||||
fun dummyNavigation(): Navigation {
|
||||
@@ -29,10 +32,11 @@ fun fakeDocument(): DocumentUiModel {
|
||||
}
|
||||
|
||||
fun fakeDocument(pageIds: ImmutableList<String>, context: Context): DocumentUiModel {
|
||||
val loader = { id:String ->
|
||||
context.assets.open(id).use { input ->
|
||||
val loader = { key: PageViewKey ->
|
||||
context.assets.open("${key.pageId}.jpg").use { 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)
|
||||
}
|
||||
val content: LazyListScope.() -> Unit = {
|
||||
itemsIndexed(state.document.pageIds, key = { _, item -> item}) { index, item ->
|
||||
ReorderableItem(reorderableLazyListState, key = item) { isDragging ->
|
||||
itemsIndexed(state.document.pageKeys, key = { _, item -> item.saveKey}) { index, item ->
|
||||
ReorderableItem(reorderableLazyListState, key = item.saveKey) { isDragging ->
|
||||
val borderColor =
|
||||
if (isDragging) MaterialTheme.colorScheme.primary else Color.Transparent
|
||||
val modifier = Modifier
|
||||
|
||||
@@ -86,6 +86,7 @@ import org.fairscan.app.MainViewModel
|
||||
import org.fairscan.app.R
|
||||
import org.fairscan.app.domain.CapturedPage
|
||||
import org.fairscan.app.domain.PageMetadata
|
||||
import org.fairscan.app.domain.Rotation.R0
|
||||
import org.fairscan.app.ui.Navigation
|
||||
import org.fairscan.app.ui.Screen
|
||||
import org.fairscan.app.ui.components.CameraPermissionState
|
||||
@@ -471,7 +472,7 @@ fun CameraScreenPreviewWithProcessedImage() {
|
||||
CapturedPage(
|
||||
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)
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.fairscan.app.AppContainer
|
||||
import org.fairscan.app.domain.CapturedPage
|
||||
import org.fairscan.app.domain.ExportQuality
|
||||
import org.fairscan.app.domain.PageMetadata
|
||||
import org.fairscan.app.domain.Rotation
|
||||
import org.fairscan.imageprocessing.Mask
|
||||
import org.fairscan.imageprocessing.Quad
|
||||
import org.fairscan.imageprocessing.detectDocumentQuad
|
||||
@@ -206,7 +207,8 @@ fun extractDocumentFromBitmap(
|
||||
val outBitmap = toBitmap(outBgr)
|
||||
outBgr.release()
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,28 +16,29 @@ package org.fairscan.app.ui.state
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import org.fairscan.app.domain.PageViewKey
|
||||
|
||||
data class DocumentUiModel(
|
||||
val pageIds: ImmutableList<String>,
|
||||
private val imageLoader: (String) -> Bitmap?,
|
||||
private val thumbnailLoader: (String) -> Bitmap?
|
||||
val pageKeys: ImmutableList<PageViewKey>,
|
||||
private val imageLoader: (PageViewKey) -> Bitmap?,
|
||||
private val thumbnailLoader: (PageViewKey) -> Bitmap?
|
||||
) {
|
||||
fun pageCount(): Int {
|
||||
return pageIds.size
|
||||
return pageKeys.size
|
||||
}
|
||||
fun pageId(index: Int): String {
|
||||
return pageIds[index]
|
||||
return pageKeys[index].pageId
|
||||
}
|
||||
fun isEmpty(): Boolean {
|
||||
return pageIds.isEmpty()
|
||||
return pageKeys.isEmpty()
|
||||
}
|
||||
fun lastIndex(): Int {
|
||||
return pageIds.lastIndex
|
||||
return pageKeys.lastIndex
|
||||
}
|
||||
fun load(index: Int): Bitmap? {
|
||||
return imageLoader(pageIds[index])
|
||||
return imageLoader(pageKeys[index])
|
||||
}
|
||||
fun loadThumbnail(index: Int): Bitmap? {
|
||||
return thumbnailLoader(pageIds[index])
|
||||
return thumbnailLoader(pageKeys[index])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user