fixed motion photo export

This commit is contained in:
Thibault Deckers 2021-04-26 19:00:45 +09:00
parent 768a077857
commit 2be011e66a
2 changed files with 71 additions and 60 deletions

View file

@ -24,6 +24,9 @@ import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BmpWriter import deckers.thibault.aves.utils.BmpWriter
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.extensionFor
import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils.copyFileToTemp import deckers.thibault.aves.utils.StorageUtils.copyFileToTemp
import deckers.thibault.aves.utils.StorageUtils.createDirectoryIfAbsent import deckers.thibault.aves.utils.StorageUtils.createDirectoryIfAbsent
import deckers.thibault.aves.utils.StorageUtils.getDocumentFile import deckers.thibault.aves.utils.StorageUtils.getDocumentFile
@ -51,11 +54,15 @@ abstract class ImageProvider {
suspend fun exportMultiple( suspend fun exportMultiple(
context: Context, context: Context,
mimeType: String, imageExportMimeType: String,
destinationDir: String, destinationDir: String,
entries: List<AvesEntry>, entries: List<AvesEntry>,
callback: ImageOpCallback, callback: ImageOpCallback,
) { ) {
if (!supportedExportMimeTypes.contains(imageExportMimeType)) {
throw Exception("unsupported export MIME type=$imageExportMimeType")
}
val destinationDirDocFile = createDirectoryIfAbsent(context, destinationDir) val destinationDirDocFile = createDirectoryIfAbsent(context, destinationDir)
if (destinationDirDocFile == null) { if (destinationDirDocFile == null) {
callback.onFailure(Exception("failed to create directory at path=$destinationDir")) callback.onFailure(Exception("failed to create directory at path=$destinationDir"))
@ -73,13 +80,15 @@ abstract class ImageProvider {
"success" to false, "success" to false,
) )
val sourceMimeType = entry.mimeType
val exportMimeType = if (isVideo(sourceMimeType)) sourceMimeType else imageExportMimeType
try { try {
val newFields = exportSingleByTreeDocAndScan( val newFields = exportSingleByTreeDocAndScan(
context = context, context = context,
sourceEntry = entry, sourceEntry = entry,
destinationDir = destinationDir, destinationDir = destinationDir,
destinationDirDocFile = destinationDirDocFile, destinationDirDocFile = destinationDirDocFile,
exportMimeType = mimeType, exportMimeType = exportMimeType,
) )
result["newFields"] = newFields result["newFields"] = newFields
result["success"] = true result["success"] = true
@ -113,13 +122,7 @@ abstract class ImageProvider {
val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}" desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
} }
val desiredFileName = desiredNameWithoutExtension + when (exportMimeType) { val desiredFileName = desiredNameWithoutExtension + extensionFor(exportMimeType)
MimeTypes.BMP -> ".bmp"
MimeTypes.JPEG -> ".jpg"
MimeTypes.PNG -> ".png"
MimeTypes.WEBP -> ".webp"
else -> throw Exception("unsupported export MIME type=$exportMimeType")
}
if (File(destinationDir, desiredFileName).exists()) { if (File(destinationDir, desiredFileName).exists()) {
throw Exception("file with name=$desiredFileName already exists in destination directory") throw Exception("file with name=$desiredFileName already exists in destination directory")
@ -133,59 +136,65 @@ abstract class ImageProvider {
val destinationTreeFile = destinationDirDocFile.createFile(exportMimeType, desiredNameWithoutExtension) val destinationTreeFile = destinationDirDocFile.createFile(exportMimeType, desiredNameWithoutExtension)
val destinationDocFile = DocumentFileCompat.fromSingleUri(context, destinationTreeFile.uri) val destinationDocFile = DocumentFileCompat.fromSingleUri(context, destinationTreeFile.uri)
val model: Any = if (MimeTypes.isHeic(sourceMimeType) && pageId != null) { if (isVideo(sourceMimeType)) {
MultiTrackImage(context, sourceUri, pageId) val sourceDocFile = DocumentFileCompat.fromSingleUri(context, sourceUri)
} else if (sourceMimeType == MimeTypes.TIFF) { @Suppress("BlockingMethodInNonBlockingContext")
TiffImage(context, sourceUri, pageId) sourceDocFile.copyTo(destinationDocFile)
} else { } else {
sourceUri val model: Any = if (MimeTypes.isHeic(sourceMimeType) && pageId != null) {
} MultiTrackImage(context, sourceUri, pageId)
} else if (sourceMimeType == MimeTypes.TIFF) {
// request a fresh image with the highest quality format TiffImage(context, sourceUri, pageId)
val glideOptions = RequestOptions() } else {
.format(DecodeFormat.PREFER_ARGB_8888) sourceUri
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
val target = Glide.with(context)
.asBitmap()
.apply(glideOptions)
.load(model)
.submit()
try {
@Suppress("BlockingMethodInNonBlockingContext")
var bitmap = target.get()
if (MimeTypes.needRotationAfterGlide(sourceMimeType)) {
bitmap = BitmapUtils.applyExifOrientation(context, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped)
} }
bitmap ?: throw Exception("failed to get image from uri=$sourceUri page=$pageId")
@Suppress("BlockingMethodInNonBlockingContext") // request a fresh image with the highest quality format
destinationDocFile.openOutputStream().use { val glideOptions = RequestOptions()
if (exportMimeType == MimeTypes.BMP) { .format(DecodeFormat.PREFER_ARGB_8888)
BmpWriter.writeRGB24(bitmap, it) .diskCacheStrategy(DiskCacheStrategy.NONE)
} else { .skipMemoryCache(true)
val quality = 100
val format = when (exportMimeType) { val target = Glide.with(context)
MimeTypes.JPEG -> Bitmap.CompressFormat.JPEG .asBitmap()
MimeTypes.PNG -> Bitmap.CompressFormat.PNG .apply(glideOptions)
MimeTypes.WEBP -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { .load(model)
if (quality == 100) { .submit()
Bitmap.CompressFormat.WEBP_LOSSLESS try {
} else { @Suppress("BlockingMethodInNonBlockingContext")
Bitmap.CompressFormat.WEBP_LOSSY var bitmap = target.get()
} if (MimeTypes.needRotationAfterGlide(sourceMimeType)) {
} else { bitmap = BitmapUtils.applyExifOrientation(context, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped)
@Suppress("DEPRECATION")
Bitmap.CompressFormat.WEBP
}
else -> throw Exception("unsupported export MIME type=$exportMimeType")
}
bitmap.compress(format, quality, it)
} }
bitmap ?: throw Exception("failed to get image from uri=$sourceUri page=$pageId")
@Suppress("BlockingMethodInNonBlockingContext")
destinationDocFile.openOutputStream().use { output ->
if (exportMimeType == MimeTypes.BMP) {
BmpWriter.writeRGB24(bitmap, output)
} else {
val quality = 100
val format = when (exportMimeType) {
MimeTypes.JPEG -> Bitmap.CompressFormat.JPEG
MimeTypes.PNG -> Bitmap.CompressFormat.PNG
MimeTypes.WEBP -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (quality == 100) {
Bitmap.CompressFormat.WEBP_LOSSLESS
} else {
Bitmap.CompressFormat.WEBP_LOSSY
}
} else {
@Suppress("DEPRECATION")
Bitmap.CompressFormat.WEBP
}
else -> throw Exception("unsupported export MIME type=$exportMimeType")
}
bitmap.compress(format, quality, output)
}
}
} finally {
Glide.with(context).clear(target)
} }
} finally {
Glide.with(context).clear(target)
} }
val fileName = destinationDocFile.name val fileName = destinationDocFile.name
@ -306,9 +315,9 @@ abstract class ImageProvider {
// but we need an image/video media URI (e.g. "content://media/external/images/media/62872") // but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
contentId = newUri.tryParseId() contentId = newUri.tryParseId()
if (contentId != null) { if (contentId != null) {
if (MimeTypes.isImage(mimeType)) { if (isImage(mimeType)) {
contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId) contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId)
} else if (MimeTypes.isVideo(mimeType)) { } else if (isVideo(mimeType)) {
contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId) contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId)
} }
} }
@ -356,5 +365,7 @@ abstract class ImageProvider {
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<ImageProvider>() private val LOG_TAG = LogUtils.createTag<ImageProvider>()
val supportedExportMimeTypes = listOf(MimeTypes.BMP, MimeTypes.JPEG, MimeTypes.PNG, MimeTypes.WEBP)
} }
} }

View file

@ -60,7 +60,7 @@ class Durations {
static const doubleBackTimerDelay = Duration(milliseconds: 1000); static const doubleBackTimerDelay = Duration(milliseconds: 1000);
static const softKeyboardDisplayDelay = Duration(milliseconds: 300); static const softKeyboardDisplayDelay = Duration(milliseconds: 300);
static const searchDebounceDelay = Duration(milliseconds: 250); static const searchDebounceDelay = Duration(milliseconds: 250);
static const contentChangeDebounceDelay = Duration(milliseconds: 500); static const contentChangeDebounceDelay = Duration(milliseconds: 1000);
// app life // app life
static const lastVersionCheckInterval = Duration(days: 7); static const lastVersionCheckInterval = Duration(days: 7);