mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-24 02:18:50 +01:00
Use lossless JPG rotation if possible
This commit is contained in:
parent
7ffe4a55b9
commit
48d3b75607
2 changed files with 150 additions and 0 deletions
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue