From 8951218cb403f0de8aae72c3a8b3757e85f05d42 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Nicolas <6371790+pynicolas@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:29:33 +0100 Subject: [PATCH] Fix rotation for pages migrated from previous versions of the app --- .../java/org/fairscan/app/MainViewModel.kt | 3 +- .../org/fairscan/app/data/ImageRepository.kt | 24 +++++++-------- .../fairscan/app/domain/ExportPreparation.kt | 6 ++-- .../main/java/org/fairscan/app/domain/Page.kt | 2 +- .../app/ui/screens/camera/CameraScreen.kt | 2 +- .../app/ui/screens/camera/CameraViewModel.kt | 2 +- .../fairscan/app/data/ImageRepositoryTest.kt | 30 +++++++++++++------ 7 files changed, 41 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/org/fairscan/app/MainViewModel.kt b/app/src/main/java/org/fairscan/app/MainViewModel.kt index b8e7e93..c3c0a2c 100644 --- a/app/src/main/java/org/fairscan/app/MainViewModel.kt +++ b/app/src/main/java/org/fairscan/app/MainViewModel.kt @@ -30,7 +30,6 @@ 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 @@ -47,7 +46,7 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode _pages.map { pages -> DocumentUiModel( pageKeys = pages.map { p -> - PageViewKey(p.id, p.metadata?.manualRotation?: Rotation.R0) + PageViewKey(p.id, p.manualRotation) }.toImmutableList(), imageLoader = ::getBitmap, thumbnailLoader = ::getThumbnail, diff --git a/app/src/main/java/org/fairscan/app/data/ImageRepository.kt b/app/src/main/java/org/fairscan/app/data/ImageRepository.kt index a145c3b..68cc8d3 100644 --- a/app/src/main/java/org/fairscan/app/data/ImageRepository.kt +++ b/app/src/main/java/org/fairscan/app/data/ImageRepository.kt @@ -122,8 +122,11 @@ class ImageRepository( } fun pages(): List = - pages.pages().map { - ScanPage(it.id, it.toMetadata()) + pages.pages().mapNotNull { + runCatching { + val manualRotation = Rotation.fromDegrees(it.manualRotationDegrees) + ScanPage(it.id, manualRotation, it.toMetadata()) + }.getOrNull() } private fun page(id: String): PageV2? = pages.get(id) @@ -140,7 +143,7 @@ class ImageRepository( id = id, quad = metadata.normalizedQuad.toSerializable(), baseRotationDegrees = metadata.baseRotation.degrees, - manualRotationDegrees = metadata.manualRotation.degrees, + manualRotationDegrees = Rotation.R0.degrees, isColored = metadata.isColored ) ) @@ -321,13 +324,10 @@ fun NormalizedQuad.toQuad(): Quad = ) fun PageV2.toMetadata(): PageMetadata? { - return runCatching { - if (quad == null || isColored == null) return null - PageMetadata( - quad.toQuad(), - Rotation.fromDegrees(baseRotationDegrees), - Rotation.fromDegrees(manualRotationDegrees), - isColored - ) - }.getOrNull() + if (quad == null || isColored == null) return null + return PageMetadata( + quad.toQuad(), + Rotation.fromDegrees(baseRotationDegrees), + isColored + ) } diff --git a/app/src/main/java/org/fairscan/app/domain/ExportPreparation.kt b/app/src/main/java/org/fairscan/app/domain/ExportPreparation.kt index 2671a52..c86ef40 100644 --- a/app/src/main/java/org/fairscan/app/domain/ExportPreparation.kt +++ b/app/src/main/java/org/fairscan/app/domain/ExportPreparation.kt @@ -45,8 +45,9 @@ fun jpegsForExport( ExportQuality.HIGH -> pages.mapNotNull { page -> val sourceJpegBytes = imageRepository.sourceJpegBytes(page.id) val pageMetadata = page.metadata + val manualRotation = page.manualRotation if (sourceJpegBytes != null && pageMetadata != null) - prepareJpegForHigh(sourceJpegBytes, pageMetadata, exportQuality) + prepareJpegForHigh(sourceJpegBytes, pageMetadata, manualRotation, exportQuality) else imageRepository.jpegBytes(page.id) } @@ -73,6 +74,7 @@ fun resizeJpegBytesForMaxPixels( fun prepareJpegForHigh( sourceJpegBytes: ByteArray, pageMetadata: PageMetadata, + manualRotation: Rotation, exportQuality: ExportQuality, ): ByteArray? { @@ -84,7 +86,7 @@ fun prepareJpegForHigh( val page = extractDocument( decoded, quad, - pageMetadata.baseRotation.add(pageMetadata.manualRotation).degrees, + pageMetadata.baseRotation.add(manualRotation).degrees, pageMetadata.isColored, exportQuality.maxPixels) val outJpegBytes = encodeJpeg(page, exportQuality.jpegQuality) diff --git a/app/src/main/java/org/fairscan/app/domain/Page.kt b/app/src/main/java/org/fairscan/app/domain/Page.kt index 849b31f..a21251d 100644 --- a/app/src/main/java/org/fairscan/app/domain/Page.kt +++ b/app/src/main/java/org/fairscan/app/domain/Page.kt @@ -19,12 +19,12 @@ import org.fairscan.imageprocessing.Quad data class PageMetadata( val normalizedQuad: Quad, val baseRotation: Rotation, - val manualRotation: Rotation, val isColored: Boolean, ) data class ScanPage( val id: String, + val manualRotation: Rotation, val metadata: PageMetadata?, ) diff --git a/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraScreen.kt b/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraScreen.kt index fee9d06..fa26a76 100644 --- a/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraScreen.kt +++ b/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraScreen.kt @@ -473,7 +473,7 @@ fun CameraScreenPreviewWithProcessedImage() { CapturedPage( 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) diff --git a/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraViewModel.kt b/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraViewModel.kt index b33bbf4..2fcc218 100644 --- a/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraViewModel.kt +++ b/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraViewModel.kt @@ -198,7 +198,7 @@ fun extractDocumentFromBitmap( outBgr.release() val normalizedQuad = quad.scaledTo(source.width, source.height, 1, 1) val baseRotation = Rotation.fromDegrees(rotationDegrees) - val metadata = PageMetadata(normalizedQuad, baseRotation, Rotation.R0, isColored) + val metadata = PageMetadata(normalizedQuad, baseRotation, isColored) return CapturedPage(outBitmap, source, metadata) } diff --git a/app/src/test/java/org/fairscan/app/data/ImageRepositoryTest.kt b/app/src/test/java/org/fairscan/app/data/ImageRepositoryTest.kt index fffc401..51068b2 100644 --- a/app/src/test/java/org/fairscan/app/data/ImageRepositoryTest.kt +++ b/app/src/test/java/org/fairscan/app/data/ImageRepositoryTest.kt @@ -38,7 +38,7 @@ class ImageRepositoryTest { private var _filesDir: File? = null 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 { if (_filesDir == null) { @@ -73,11 +73,11 @@ class ImageRepositoryTest { val page = repo.pages().first() assertThat(page.id).isEqualTo(id) + assertThat(page.manualRotation).isEqualTo(R0) val metadata = page.metadata assertThat(metadata).isNotNull() assertThat(metadata!!.normalizedQuad).isEqualTo(quad1) assertThat(metadata.baseRotation).isEqualTo(metadata1.baseRotation) - assertThat(metadata.manualRotation).isEqualTo(metadata1.manualRotation) assertThat(metadata.isColored).isEqualTo(metadata1.isColored) } @@ -192,19 +192,18 @@ class ImageRepositoryTest { fun rotate() { val repo = repo() repo.add(byteArrayOf(101, 102, 103), byteArrayOf(51), metadata1) - assertThat(metadata1.manualRotation).isEqualTo(R0) assertThat(repo.pages().last().metadata).isEqualTo(metadata1) val id = repo.pages().last().id 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) - assertThat(repo.pages().last().metadata).isEqualTo(metadata1.copy(manualRotation = R180)) + assertThat(repo.pages().last().manualRotation).isEqualTo(R180) 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) - assertThat(repo.pages().last().metadata).isEqualTo(metadata1.copy(manualRotation = R0)) + assertThat(repo.pages().last().manualRotation).isEqualTo(R0) repo.rotate(id, false) - assertThat(repo.pages().last().metadata).isEqualTo(metadata1.copy(manualRotation = R270)) + assertThat(repo.pages().last().manualRotation).isEqualTo(R270) } @Test @@ -248,8 +247,21 @@ class ImageRepositoryTest { assertThat(metadata).isNotNull() 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