Avoid chaining rotations (to avoid JPEG recompressions)
This commit is contained in:
@@ -19,6 +19,7 @@ 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.ExportQuality
|
||||
import org.fairscan.app.domain.PageMetadata
|
||||
import org.fairscan.app.domain.PageViewKey
|
||||
import org.fairscan.app.domain.Rotation
|
||||
@@ -62,6 +63,7 @@ class ImageRepository(
|
||||
}
|
||||
|
||||
private fun loadPages(): MutableList<PageV2> {
|
||||
normalizeLegacyFiles()
|
||||
val filesOnDisk = scanDir.listFiles()
|
||||
?.filter { it.extension == "jpg" }
|
||||
?.map { it.name }
|
||||
@@ -176,13 +178,14 @@ class ImageRepository(
|
||||
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())
|
||||
val inputFile = File(scanDir, "$id.jpg")
|
||||
if (!inputFile.exists()) {
|
||||
return
|
||||
transformations.rotate(inputFile, outputFile, clockwise)
|
||||
}
|
||||
val outputFile = File(scanDir, workFileName(id, newManualRotation.degrees))
|
||||
if (!outputFile.exists()) {
|
||||
val jpegQuality = ExportQuality.BALANCED.jpegQuality
|
||||
transformations.rotate(inputFile, outputFile, newManualRotation.degrees, jpegQuality)
|
||||
}
|
||||
|
||||
val updated = page.copy(manualRotationDegrees = newManualRotation.degrees)
|
||||
@@ -276,6 +279,35 @@ class ImageRepository(
|
||||
file -> file.delete()
|
||||
}
|
||||
}
|
||||
|
||||
data class DiskPageFiles(
|
||||
val base: File?,
|
||||
val rotated: List<File>
|
||||
)
|
||||
|
||||
private fun normalizeLegacyFiles() {
|
||||
val jpgs = scanDir.listFiles()?.filter { it.extension == "jpg" }.orEmpty()
|
||||
val byId = jpgs.groupBy { file ->
|
||||
val name = file.name.removeSuffix(".jpg")
|
||||
val dash = name.lastIndexOf('-')
|
||||
if (dash >= 0) name.substring(0, dash) else name
|
||||
}
|
||||
val pages = byId.mapValues { (_, files) ->
|
||||
val base = files.find { !it.name.contains('-') }
|
||||
val rotated = files.filter { it.name.contains('-') }
|
||||
DiskPageFiles(base, rotated)
|
||||
}
|
||||
pages.forEach { (id, files) ->
|
||||
if (files.base == null && files.rotated.isNotEmpty()) {
|
||||
val sortedRotatedFiles = files.rotated.sortedBy { it.name }
|
||||
val legacyFile = sortedRotatedFiles.first()
|
||||
val target = File(scanDir, "$id.jpg")
|
||||
if (legacyFile.renameTo(target)) {
|
||||
sortedRotatedFiles.drop(1).forEach { it.delete() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Quad.toSerializable(): NormalizedQuad =
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.io.File
|
||||
|
||||
interface ImageTransformations {
|
||||
|
||||
fun rotate(inputFile: File, outputFile: File, clockwise: Boolean)
|
||||
fun rotate(inputFile: File, outputFile: File, rotationDegrees: Int, jpegQuality: Int)
|
||||
|
||||
fun resize(inputFile: File, outputFile: File, maxSize: Int)
|
||||
|
||||
|
||||
@@ -16,29 +16,31 @@ package org.fairscan.app.platform
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import org.opencv.core.Core
|
||||
import org.opencv.core.Mat
|
||||
import androidx.core.graphics.scale
|
||||
import org.fairscan.app.data.ImageTransformations
|
||||
import org.opencv.core.MatOfInt
|
||||
import org.opencv.imgcodecs.Imgcodecs
|
||||
import java.io.File
|
||||
import kotlin.math.min
|
||||
import androidx.core.graphics.scale
|
||||
import org.fairscan.app.data.ImageTransformations
|
||||
|
||||
class OpenCvTransformations : ImageTransformations {
|
||||
override fun rotate(inputFile: File, outputFile: File, clockwise: Boolean) {
|
||||
val src: Mat = Imgcodecs.imread(inputFile.absolutePath)
|
||||
override fun rotate(
|
||||
inputFile: File,
|
||||
outputFile: File,
|
||||
rotationDegrees: Int,
|
||||
jpegQuality: Int
|
||||
) {
|
||||
val src = Imgcodecs.imread(inputFile.absolutePath)
|
||||
require(!src.empty()) { "Could not load image from ${inputFile.absolutePath}" }
|
||||
|
||||
require (!src.empty()) { "Could not load image from ${inputFile.absolutePath}" }
|
||||
val dst = org.fairscan.imageprocessing.rotate(src, rotationDegrees)
|
||||
|
||||
val dst = Mat()
|
||||
Core.rotate(src, dst,
|
||||
if (clockwise) Core.ROTATE_90_CLOCKWISE else Core.ROTATE_90_COUNTERCLOCKWISE
|
||||
)
|
||||
|
||||
if (!Imgcodecs.imwrite(outputFile.absolutePath, dst)) {
|
||||
val params = MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, jpegQuality)
|
||||
if (!Imgcodecs.imwrite(outputFile.absolutePath, dst, params)) {
|
||||
throw RuntimeException("Could not write image to ${outputFile.absolutePath}")
|
||||
}
|
||||
|
||||
params.release()
|
||||
src.release()
|
||||
dst.release()
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class ImageRepositoryTest {
|
||||
|
||||
fun repo(): ImageRepository {
|
||||
val transformations = object : ImageTransformations {
|
||||
override fun rotate(inputFile: File, outputFile: File, clockwise: Boolean) {
|
||||
override fun rotate(inputFile: File, outputFile: File, rotationDegrees: Int, jpegQuality: Int) {
|
||||
inputFile.copyTo(outputFile)
|
||||
}
|
||||
override fun resize(inputFile: File, outputFile: File, maxSize: Int) {
|
||||
@@ -130,6 +130,18 @@ class ImageRepositoryTest {
|
||||
assertThat(repo().imageIds()).containsExactly("2")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should rename rotated files with no base file`() {
|
||||
scanDir().mkdirs()
|
||||
val bytes = byteArrayOf(105, 106, 107)
|
||||
File(scanDir(), "123-90.jpg").writeBytes(bytes)
|
||||
File(scanDir(), "123-270.jpg").writeBytes(bytes)
|
||||
val repo = repo()
|
||||
assertThat(repo.imageIds()).containsExactly("123")
|
||||
val jpegFiles = jpegFiles(scanDir())
|
||||
assertThat(jpegFiles).hasSize(1).allMatch { it?.name == "123.jpg" }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return null on invalid id`() {
|
||||
val repo = repo()
|
||||
|
||||
Reference in New Issue
Block a user