Fix rotation for pages migrated from previous versions of the app

This commit is contained in:
Pierre-Yves Nicolas
2026-01-17 20:29:33 +01:00
parent 7d74c1cd46
commit 8951218cb4
7 changed files with 41 additions and 28 deletions

View File

@@ -30,7 +30,6 @@ 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.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
@@ -47,7 +46,7 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
_pages.map { pages -> _pages.map { pages ->
DocumentUiModel( DocumentUiModel(
pageKeys = pages.map { p -> pageKeys = pages.map { p ->
PageViewKey(p.id, p.metadata?.manualRotation?: Rotation.R0) PageViewKey(p.id, p.manualRotation)
}.toImmutableList(), }.toImmutableList(),
imageLoader = ::getBitmap, imageLoader = ::getBitmap,
thumbnailLoader = ::getThumbnail, thumbnailLoader = ::getThumbnail,

View File

@@ -122,8 +122,11 @@ class ImageRepository(
} }
fun pages(): List<ScanPage> = fun pages(): List<ScanPage> =
pages.pages().map { pages.pages().mapNotNull {
ScanPage(it.id, it.toMetadata()) runCatching {
val manualRotation = Rotation.fromDegrees(it.manualRotationDegrees)
ScanPage(it.id, manualRotation, it.toMetadata())
}.getOrNull()
} }
private fun page(id: String): PageV2? = pages.get(id) private fun page(id: String): PageV2? = pages.get(id)
@@ -140,7 +143,7 @@ class ImageRepository(
id = id, id = id,
quad = metadata.normalizedQuad.toSerializable(), quad = metadata.normalizedQuad.toSerializable(),
baseRotationDegrees = metadata.baseRotation.degrees, baseRotationDegrees = metadata.baseRotation.degrees,
manualRotationDegrees = metadata.manualRotation.degrees, manualRotationDegrees = Rotation.R0.degrees,
isColored = metadata.isColored isColored = metadata.isColored
) )
) )
@@ -321,13 +324,10 @@ fun NormalizedQuad.toQuad(): Quad =
) )
fun PageV2.toMetadata(): PageMetadata? { fun PageV2.toMetadata(): PageMetadata? {
return runCatching {
if (quad == null || isColored == null) return null if (quad == null || isColored == null) return null
PageMetadata( return PageMetadata(
quad.toQuad(), quad.toQuad(),
Rotation.fromDegrees(baseRotationDegrees), Rotation.fromDegrees(baseRotationDegrees),
Rotation.fromDegrees(manualRotationDegrees),
isColored isColored
) )
}.getOrNull()
} }

View File

@@ -45,8 +45,9 @@ fun jpegsForExport(
ExportQuality.HIGH -> pages.mapNotNull { page -> ExportQuality.HIGH -> pages.mapNotNull { page ->
val sourceJpegBytes = imageRepository.sourceJpegBytes(page.id) val sourceJpegBytes = imageRepository.sourceJpegBytes(page.id)
val pageMetadata = page.metadata val pageMetadata = page.metadata
val manualRotation = page.manualRotation
if (sourceJpegBytes != null && pageMetadata != null) if (sourceJpegBytes != null && pageMetadata != null)
prepareJpegForHigh(sourceJpegBytes, pageMetadata, exportQuality) prepareJpegForHigh(sourceJpegBytes, pageMetadata, manualRotation, exportQuality)
else else
imageRepository.jpegBytes(page.id) imageRepository.jpegBytes(page.id)
} }
@@ -73,6 +74,7 @@ fun resizeJpegBytesForMaxPixels(
fun prepareJpegForHigh( fun prepareJpegForHigh(
sourceJpegBytes: ByteArray, sourceJpegBytes: ByteArray,
pageMetadata: PageMetadata, pageMetadata: PageMetadata,
manualRotation: Rotation,
exportQuality: ExportQuality, exportQuality: ExportQuality,
): ByteArray? { ): ByteArray? {
@@ -84,7 +86,7 @@ fun prepareJpegForHigh(
val page = extractDocument( val page = extractDocument(
decoded, decoded,
quad, quad,
pageMetadata.baseRotation.add(pageMetadata.manualRotation).degrees, pageMetadata.baseRotation.add(manualRotation).degrees,
pageMetadata.isColored, pageMetadata.isColored,
exportQuality.maxPixels) exportQuality.maxPixels)
val outJpegBytes = encodeJpeg(page, exportQuality.jpegQuality) val outJpegBytes = encodeJpeg(page, exportQuality.jpegQuality)

View File

@@ -19,12 +19,12 @@ import org.fairscan.imageprocessing.Quad
data class PageMetadata( data class PageMetadata(
val normalizedQuad: Quad, val normalizedQuad: Quad,
val baseRotation: Rotation, val baseRotation: Rotation,
val manualRotation: Rotation,
val isColored: Boolean, val isColored: Boolean,
) )
data class ScanPage( data class ScanPage(
val id: String, val id: String,
val manualRotation: Rotation,
val metadata: PageMetadata?, val metadata: PageMetadata?,
) )

View File

@@ -473,7 +473,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, R0, R0, false)))) PageMetadata(quad, R0, false))))
} }
@Preview(showBackground = true, widthDp = 640, heightDp = 320) @Preview(showBackground = true, widthDp = 640, heightDp = 320)

View File

@@ -198,7 +198,7 @@ fun extractDocumentFromBitmap(
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 baseRotation = Rotation.fromDegrees(rotationDegrees) val baseRotation = Rotation.fromDegrees(rotationDegrees)
val metadata = PageMetadata(normalizedQuad, baseRotation, Rotation.R0, isColored) val metadata = PageMetadata(normalizedQuad, baseRotation, isColored)
return CapturedPage(outBitmap, source, metadata) return CapturedPage(outBitmap, source, metadata)
} }

View File

@@ -38,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, R90, R0, true) val metadata1 = PageMetadata(quad1, R90, true)
fun getFilesDir(): File { fun getFilesDir(): File {
if (_filesDir == null) { if (_filesDir == null) {
@@ -73,11 +73,11 @@ class ImageRepositoryTest {
val page = repo.pages().first() val page = repo.pages().first()
assertThat(page.id).isEqualTo(id) assertThat(page.id).isEqualTo(id)
assertThat(page.manualRotation).isEqualTo(R0)
val metadata = page.metadata val metadata = page.metadata
assertThat(metadata).isNotNull() assertThat(metadata).isNotNull()
assertThat(metadata!!.normalizedQuad).isEqualTo(quad1) assertThat(metadata!!.normalizedQuad).isEqualTo(quad1)
assertThat(metadata.baseRotation).isEqualTo(metadata1.baseRotation) assertThat(metadata.baseRotation).isEqualTo(metadata1.baseRotation)
assertThat(metadata.manualRotation).isEqualTo(metadata1.manualRotation)
assertThat(metadata.isColored).isEqualTo(metadata1.isColored) assertThat(metadata.isColored).isEqualTo(metadata1.isColored)
} }
@@ -192,19 +192,18 @@ 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)
assertThat(metadata1.manualRotation).isEqualTo(R0)
assertThat(repo.pages().last().metadata).isEqualTo(metadata1) assertThat(repo.pages().last().metadata).isEqualTo(metadata1)
val id = repo.pages().last().id val id = repo.pages().last().id
repo.rotate(id, true) repo.rotate(id, true)
assertThat(repo.pages().last().metadata).isEqualTo(metadata1.copy(manualRotation = R90)) assertThat(repo.pages().last().manualRotation).isEqualTo(R90)
repo.rotate(id, true) repo.rotate(id, true)
assertThat(repo.pages().last().metadata).isEqualTo(metadata1.copy(manualRotation = R180)) assertThat(repo.pages().last().manualRotation).isEqualTo(R180)
repo.rotate(id, true) repo.rotate(id, true)
assertThat(repo.pages().last().metadata).isEqualTo(metadata1.copy(manualRotation = R270)) assertThat(repo.pages().last().manualRotation).isEqualTo(R270)
repo.rotate(id, true) repo.rotate(id, true)
assertThat(repo.pages().last().metadata).isEqualTo(metadata1.copy(manualRotation = R0)) assertThat(repo.pages().last().manualRotation).isEqualTo(R0)
repo.rotate(id, false) repo.rotate(id, false)
assertThat(repo.pages().last().metadata).isEqualTo(metadata1.copy(manualRotation = R270)) assertThat(repo.pages().last().manualRotation).isEqualTo(R270)
} }
@Test @Test
@@ -248,8 +247,21 @@ class ImageRepositoryTest {
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() @Test
fun `pages with invalid metadata should be skipped`() {
val bytes = byteArrayOf(105, 106, 107)
writeDocumentDotJson("""{"version":2, "pages":[{"id":"1", "manualRotationDegrees":90}]}""")
File(scanDir(), "1.jpg").writeBytes(byteArrayOf(101))
File(scanDir(), "1-90.jpg").writeBytes(bytes)
assertThat(repo().imageIds()).containsExactly("1")
writeDocumentDotJson("""{"version":2, "pages":[{"id":"1", "manualRotationDegrees":42}]}""")
File(scanDir(), "1.jpg").writeBytes(byteArrayOf(101))
File(scanDir(), "1-42.jpg").writeBytes(bytes)
assertThat(repo().imageIds()).isEmpty()
} }
@Test @Test