Use lossless JPG rotation if possible

This commit is contained in:
Ming Ming 2022-09-06 16:30:45 +08:00
parent 7ffe4a55b9
commit 48d3b75607
2 changed files with 150 additions and 0 deletions

View file

@ -13,6 +13,7 @@ import android.os.AsyncTask
import android.os.Bundle
import android.os.IBinder
import android.os.PowerManager
import android.webkit.MimeTypeMap
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@ -617,6 +618,26 @@ private open class ImageProcessorCommandTask(context: Context) :
private fun handleCommand(cmd: ImageProcessorImageCommand): Uri {
val file = downloadFile(cmd.fileUrl, cmd.headers)
handleCancel()
// special case for lossless rotation
if (cmd is ImageProcessorFilterCommand) {
if (shouldTryLosslessRotate(cmd, cmd.filename)) {
val filter = cmd.filters.first() as Orientation
try {
return loselessRotate(
filter.degree, file, cmd.filename,
"Edited Photos"
)
} catch (e: Throwable) {
logE(
TAG,
"[handleCommand] Lossless rotation has failed, fallback to lossy",
e
)
}
}
}
return try {
val fileUri = Uri.fromFile(file)
val output = measureTime(TAG, "[handleCommand] Elapsed time", {
@ -636,6 +657,53 @@ private open class ImageProcessorCommandTask(context: Context) :
}
}
private fun shouldTryLosslessRotate(
cmd: ImageProcessorFilterCommand, srcFilename: String
): Boolean {
try {
if (cmd.filters.size != 1) {
return false
}
if (cmd.filters.first() !is Orientation) {
return false
}
// we can't use the content resolver here because the file we just
// downloaded does not exist in the media store
val ext = srcFilename.split('.').last()
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)
logD(TAG, "[shouldTryLosslessRotate] ext: $ext -> mime: $mime")
return mime == "image/jpeg"
} catch (e: Throwable) {
logE(TAG, "[shouldTryLosslessRotate] Uncaught exception", e)
return false
}
}
private fun loselessRotate(
degree: Int, srcFile: File, outFilename: String, subDir: String
): Uri {
logI(TAG, "[loselessRotate] $outFilename")
val outFile = File.createTempFile("out", null, getTempDir(context))
try {
srcFile.copyTo(outFile, overwrite = true)
val iExif = ExifInterface(srcFile)
val oExif = ExifInterface(outFile)
copyExif(iExif, oExif)
LosslessRotator()(degree, iExif, oExif)
oExif.saveAttributes()
handleCancel()
// move file to user accessible storage
val uri = MediaStoreUtil.copyFileToDownload(
context, Uri.fromFile(outFile), outFilename,
"Photos (for Nextcloud)/$subDir"
)
return uri
} finally {
outFile.delete()
}
}
private fun downloadFile(
fileUrl: String, headers: Map<String, String>?
): File {

View file

@ -0,0 +1,82 @@
package com.nkming.nc_photos.plugin.image_processor
import androidx.exifinterface.media.ExifInterface
import com.nkming.nc_photos.plugin.logI
/**
* Lossless rotation is done by modifying the EXIF orientation tag in such a way
* that the viewer will rotate the image when displaying the image
*/
class LosslessRotator {
companion object {
const val TAG = "LosslessRotator"
}
/**
* Set the Orientation tag in @a dstExif according to the value in
* @a srcExif
*
* @param degree Either 0, 90, 180, -90 or -180
* @param srcExif ExifInterface of the src file
* @param dstExif ExifInterface of the dst file
*/
operator fun invoke(
degree: Int, srcExif: ExifInterface, dstExif: ExifInterface
) {
assert(degree in listOf(0, 90, 180, -90, -180))
val srcOrientation =
srcExif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1)
val dstOrientation = rotateExifOrientationValue(srcOrientation, degree)
logI(TAG, "[invoke] $degree, $srcOrientation -> $dstOrientation")
dstExif.setAttribute(
ExifInterface.TAG_ORIENTATION, dstOrientation.toString()
)
}
/**
* Return a new orientation representing the resulting value after rotating
* @a value
*
* @param value
* @param degree Either 0, 90, 180, -90 or -180
* @return
*/
private fun rotateExifOrientationValue(value: Int, degree: Int): Int {
if (degree == 0) {
return value
}
var newValue = rotateExifOrientationValue90Ccw(value)
if (degree == 90) {
return newValue
}
newValue = rotateExifOrientationValue90Ccw(newValue)
if (degree == 180 || degree == -180) {
return newValue
}
newValue = rotateExifOrientationValue90Ccw(newValue)
return newValue
}
/**
* Return a new orientation representing the resulting value after rotating
* @a value for 90 degree CCW
*
* @param value
* @return
*/
private fun rotateExifOrientationValue90Ccw(value: Int): Int {
return when (value) {
0, 1 -> 8
8 -> 3
3 -> 6
6 -> 1
2 -> 7
7 -> 4
4 -> 5
5 -> 2
else -> throw IllegalArgumentException(
"Invalid EXIF Orientation value: $value"
)
}
}
}