mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-24 10:28: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.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.core.app.NotificationChannelCompat
|
import androidx.core.app.NotificationChannelCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
@ -617,6 +618,26 @@ private open class ImageProcessorCommandTask(context: Context) :
|
||||||
private fun handleCommand(cmd: ImageProcessorImageCommand): Uri {
|
private fun handleCommand(cmd: ImageProcessorImageCommand): Uri {
|
||||||
val file = downloadFile(cmd.fileUrl, cmd.headers)
|
val file = downloadFile(cmd.fileUrl, cmd.headers)
|
||||||
handleCancel()
|
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 {
|
return try {
|
||||||
val fileUri = Uri.fromFile(file)
|
val fileUri = Uri.fromFile(file)
|
||||||
val output = measureTime(TAG, "[handleCommand] Elapsed time", {
|
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(
|
private fun downloadFile(
|
||||||
fileUrl: String, headers: Map<String, String>?
|
fileUrl: String, headers: Map<String, String>?
|
||||||
): File {
|
): 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