Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
deckerst committed Dec 21, 2023
2 parents 5b6d7af + b010e2f commit 32fff62
Show file tree
Hide file tree
Showing 204 changed files with 3,707 additions and 1,696 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.

## <a id="unreleased"></a>[Unreleased]

## <a id="v1.10.1"></a>[v1.10.1] - 2023-12-21

- Cataloguing: detect/filter `Ultra HDR`
- Viewer: show JPEG MPF dependent images (except thumbnails and HDR gain maps)
- Info: show metadata from JPEG MPF
- Info: open images embedded via JPEG MPF
- Arabic translation (thanks Mohamed Zeroug)
- Belarusian translation (thanks Макар Разин)

### Changed

- upgraded Flutter to stable v3.16.5

## <a id="v1.10.0"></a>[v1.10.0] - 2023-12-02

### Added
Expand Down
5 changes: 3 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ dependencies {

implementation "androidx.appcompat:appcompat:1.6.1"
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.exifinterface:exifinterface:1.3.6'
implementation 'androidx.exifinterface:exifinterface:1.3.7'
implementation 'androidx.lifecycle:lifecycle-process:2.6.2'
implementation 'androidx.media:media:1.7.0'
implementation 'androidx.multidex:multidex:2.0.1'
Expand All @@ -225,6 +225,7 @@ dependencies {
implementation 'com.commonsware.cwac:document:0.5.0'
implementation 'com.drewnoakes:metadata-extractor:2.19.0'
implementation "com.github.bumptech.glide:glide:$glide_version"
implementation 'com.google.android.material:material:1.11.0'
// SLF4J implementation for `mp4parser`
implementation 'org.slf4j:slf4j-simple:2.0.9'

Expand All @@ -242,7 +243,7 @@ dependencies {

testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.1'

kapt 'androidx.annotation:annotation:1.7.0'
kapt 'androidx.annotation:annotation:1.7.1'
ksp "com.github.bumptech.glide:ksp:$glide_version"

compileOnly rootProject.findProject(':streams_channel')
Expand Down
3 changes: 3 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.hardware.wifi"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import android.os.Build
import android.provider.MediaStore
import android.provider.Settings
import androidx.core.content.pm.ShortcutManagerCompat
import com.google.android.material.color.DynamicColors
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.model.FieldMap
import io.flutter.plugin.common.MethodCall
Expand All @@ -18,7 +19,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.util.*
import java.util.Locale
import java.util.TimeZone

class DeviceHandler(private val context: Context) : MethodCallHandler {
private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
Expand Down Expand Up @@ -52,7 +54,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
"canSetLockScreenWallpaper" to (sdkInt >= Build.VERSION_CODES.N),
"canUseCrypto" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
"hasGeocoder" to Geocoder.isPresent(),
"isDynamicColorAvailable" to (sdkInt >= Build.VERSION_CODES.S),
"isDynamicColorAvailable" to DynamicColors.isDynamicColorAvailable(),
"showPinShortcutFeedback" to (sdkInt >= Build.VERSION_CODES.O),
"supportEdgeToEdgeUIMode" to (sdkInt >= Build.VERSION_CODES.Q),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,28 @@ import com.bumptech.glide.load.resource.bitmap.TransformationUtils
import com.drew.metadata.xmp.XmpDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import deckers.thibault.aves.metadata.*
import deckers.thibault.aves.metadata.GoogleDeviceContainer
import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.metadata.MultiPage
import deckers.thibault.aves.metadata.XMP
import deckers.thibault.aves.metadata.XMP.doesPropPathExist
import deckers.thibault.aves.metadata.XMP.getSafeStructField
import deckers.thibault.aves.metadata.XMPPropName
import deckers.thibault.aves.metadata.metadataextractor.Helper
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.provider.ContentImageProvider
import deckers.thibault.aves.model.provider.ImageProvider
import deckers.thibault.aves.utils.*
import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider
import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.FileUtils.transferFrom
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
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
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
Expand All @@ -42,6 +49,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
when (call.method) {
"getExifThumbnails" -> ioScope.launch { safeSuspend(call, result, ::getExifThumbnails) }
"extractGoogleDeviceItem" -> ioScope.launch { safe(call, result, ::extractGoogleDeviceItem) }
"extractJpegMpfItem" -> ioScope.launch { safe(call, result, ::extractJpegMpfItem) }
"extractMotionPhotoImage" -> ioScope.launch { safe(call, result, ::extractMotionPhotoImage) }
"extractMotionPhotoVideo" -> ioScope.launch { safe(call, result, ::extractMotionPhotoVideo) }
"extractVideoEmbeddedPicture" -> ioScope.launch { safe(call, result, ::extractVideoEmbeddedPicture) }
Expand Down Expand Up @@ -141,6 +149,40 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
result.error("extractGoogleDeviceItem-empty", "failed to extract item from Google Device XMP at uri=$uri dataUri=$dataUri", null)
}

private fun extractJpegMpfItem(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
val displayName = call.argument<String>("displayName")
val id = call.argument<Int>("id")
if (mimeType == null || uri == null || sizeBytes == null || id == null) {
result.error("extractJpegMpfItem-args", "missing arguments", null)
return
}

val pageIndex = id - 1
val mpEntries = MultiPage.getJpegMpfEntries(context, uri)
if (mpEntries != null && pageIndex < mpEntries.size) {
val mpEntry = mpEntries[pageIndex]
mpEntry.mimeType?.let { embedMimeType ->
var dataOffset = mpEntry.dataOffset
if (dataOffset > 0) {
val baseOffset = MultiPage.getJpegMpfBaseOffset(context, uri)
if (baseOffset != null) {
dataOffset += baseOffset
}
}
StorageUtils.openInputStream(context, uri)?.let { input ->
input.skip(dataOffset)
copyEmbeddedBytes(result, embedMimeType, displayName, input, mpEntry.size)
}
return
}
}

result.error("extractJpegMpfItem-empty", "failed to extract file index=$id from MPF at uri=$uri", null)
}

private fun extractMotionPhotoImage(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
Expand Down Expand Up @@ -299,8 +341,14 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
"mimeType" to mimeType,
)
if (isImage(mimeType) || isVideo(mimeType)) {
val provider = getProvider(context, uri)
if (provider == null) {
result.error("copyEmbeddedBytes-provider", "failed to find provider for uri=$uri", null)
return
}

ioScope.launch {
ContentImageProvider().fetchSingle(context, uri, mimeType, object : ImageProvider.ImageOpCallback {
provider.fetchSingle(context, uri, mimeType, object : ImageProvider.ImageOpCallback {
override fun onSuccess(fields: FieldMap) {
resultFields.putAll(fields)
result.success(resultFields)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class MediaEditHandler(private val contextWrapper: ContextWrapper) : MethodCallH
return
}

val provider = getProvider(uri)
val provider = getProvider(contextWrapper, uri)
if (provider == null) {
result.error("captureFrame-provider", "failed to find provider for uri=$uri", null)
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class MediaFetchObjectHandler(private val context: Context) : MethodCallHandler
return
}

val provider = getProvider(uri)
val provider = getProvider(context, uri)
if (provider == null) {
result.error("getEntry-provider", "failed to find provider for uri=$uri", null)
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return
}

val provider = getProvider(uri)
val provider = getProvider(contextWrapper, uri)
if (provider == null) {
result.error("editOrientation-provider", "failed to find provider for uri=$uri", null)
return
Expand Down Expand Up @@ -90,7 +90,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return
}

val provider = getProvider(uri)
val provider = getProvider(contextWrapper, uri)
if (provider == null) {
result.error("editDate-provider", "failed to find provider for uri=$uri", null)
return
Expand All @@ -117,7 +117,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return
}

val provider = getProvider(uri)
val provider = getProvider(contextWrapper, uri)
if (provider == null) {
result.error("editMetadata-provider", "failed to find provider for uri=$uri", null)
return
Expand All @@ -142,7 +142,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return
}

val provider = getProvider(uri)
val provider = getProvider(contextWrapper, uri)
if (provider == null) {
result.error("removeTrailerVideo-provider", "failed to find provider for uri=$uri", null)
return
Expand All @@ -168,7 +168,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return
}

val provider = getProvider(uri)
val provider = getProvider(contextWrapper, uri)
if (provider == null) {
result.error("removeTypes-provider", "failed to find provider for uri=$uri", null)
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import deckers.thibault.aves.metadata.XMP.getSafeDateMillis
import deckers.thibault.aves.metadata.XMP.getSafeInt
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
import deckers.thibault.aves.metadata.XMP.getSafeString
import deckers.thibault.aves.metadata.XMP.hasHdrGainMap
import deckers.thibault.aves.metadata.XMP.isMotionPhoto
import deckers.thibault.aves.metadata.XMP.isPanorama
import deckers.thibault.aves.metadata.metadataextractor.Helper
Expand All @@ -76,6 +77,7 @@ import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeRational
import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeString
import deckers.thibault.aves.metadata.metadataextractor.Helper.isPngTextDir
import deckers.thibault.aves.metadata.metadataextractor.PngActlDirectory
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue
import deckers.thibault.aves.utils.LogUtils
Expand Down Expand Up @@ -225,7 +227,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
foundMp4Uuid = metadata.directories.any { it is Mp4UuidBoxDirectory && it.tagCount > 0 }

val dirByName = metadata.directories.filter {
(it.tagCount > 0 || it.errorCount > 0)
(it.tagCount > 0 || it.errorCount > 0 || it is MpEntryDirectory)
&& it !is FileTypeDirectory
&& it !is AviDirectory
}.groupBy { dir -> dir.name }
Expand Down Expand Up @@ -344,6 +346,10 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
}

dir is MpEntryDirectory -> {
dirMap.putAll(dir.describe())
}

else -> dirMap.putAll(tags.map { Pair(it.tagName, it.description) })
}
}
Expand Down Expand Up @@ -551,6 +557,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (xmpMeta.isMotionPhoto()) {
flags = flags or MASK_IS_MULTIPAGE or MASK_IS_MOTION_PHOTO
}

// identification of embedded gain map
if (xmpMeta.hasHdrGainMap()) {
flags = flags or MASK_HAS_HDR_GAIN_MAP
}
} catch (e: XMPException) {
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
}
Expand Down Expand Up @@ -623,6 +634,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
}

// JPEG Multi-Picture Format
if (metadata.getDirectoriesOfType(MpEntryDirectory::class.java).count { !it.entry.isThumbnail } > 1) {
flags = flags or MASK_IS_MULTIPAGE
}

// XMP
if (!isLargeMp4(mimeType, sizeBytes)) {
metadata.getDirectoriesOfType(XmpDirectory::class.java).map { it.xmpMeta }.forEach {
Expand Down Expand Up @@ -913,10 +929,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}

val pages: ArrayList<FieldMap>? = if (isMotionPhoto) {
MultiPage.getMotionPhotoPages(context, uri, mimeType, sizeBytes = sizeBytes)
MultiPage.getMotionPhotoPages(context, uri, mimeType, sizeBytes)
} else {
when (mimeType) {
MimeTypes.HEIC, MimeTypes.HEIF -> MultiPage.getHeicTracks(context, uri)
MimeTypes.JPEG -> MultiPage.getJpegMpfPages(context, uri)
MimeTypes.TIFF -> MultiPage.getTiffPages(context, uri)
else -> null
}
Expand Down Expand Up @@ -1297,6 +1314,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private const val MASK_IS_360 = 1 shl 3
private const val MASK_IS_MULTIPAGE = 1 shl 4
private const val MASK_IS_MOTION_PHOTO = 1 shl 5
private const val MASK_HAS_HDR_GAIN_MAP = 1 shl 6
private const val XMP_SUBJECTS_SEPARATOR = ";"

// overlay metadata
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.decoder.MultiTrackImage
import deckers.thibault.aves.decoder.MultiPageImage
import deckers.thibault.aves.utils.BitmapRegionDecoderCompat
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.MimeTypes
Expand Down Expand Up @@ -40,10 +40,10 @@ class RegionFetcher internal constructor(
imageHeight: Int,
result: MethodChannel.Result,
) {
if (MimeTypes.isHeic(mimeType) && pageId != null) {
if (pageId != null && MultiPageImage.isSupported(mimeType)) {
val id = Pair(uri, pageId)
fetch(
uri = pageTempUris.getOrPut(id) { createJpegForPage(uri, pageId) },
uri = pageTempUris.getOrPut(id) { createJpegForPage(uri, mimeType, pageId) },
mimeType = MimeTypes.JPEG,
pageId = null,
sampleSize = sampleSize,
Expand Down Expand Up @@ -104,11 +104,11 @@ class RegionFetcher internal constructor(
}
}

private fun createJpegForPage(sourceUri: Uri, pageId: Int): Uri {
private fun createJpegForPage(sourceUri: Uri, mimeType: String, pageId: Int): Uri {
val target = Glide.with(context)
.asBitmap()
.apply(multiTrackGlideOptions)
.load(MultiTrackImage(context, sourceUri, pageId))
.load(MultiPageImage(context, sourceUri, mimeType, pageId))
.submit()
try {
val bitmap = target.get()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.signature.ObjectKey
import deckers.thibault.aves.decoder.MultiTrackImage
import deckers.thibault.aves.decoder.MultiPageImage
import deckers.thibault.aves.decoder.SvgImage
import deckers.thibault.aves.decoder.TiffImage
import deckers.thibault.aves.decoder.VideoThumbnail
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.SVG
import deckers.thibault.aves.utils.MimeTypes.isHeic
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterContentResolverThumbnail
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
Expand All @@ -47,8 +46,8 @@ class ThumbnailFetcher internal constructor(
private val height: Int = if (height?.takeIf { it > 0 } != null) height else defaultSize
private val svgFetch = mimeType == SVG
private val tiffFetch = mimeType == MimeTypes.TIFF
private val multiTrackFetch = isHeic(mimeType) && pageId != null
private val customFetch = svgFetch || tiffFetch || multiTrackFetch
private val multiPageFetch = pageId != null && MultiPageImage.isSupported(mimeType)
private val customFetch = svgFetch || tiffFetch || multiPageFetch

suspend fun fetch() {
var bitmap: Bitmap? = null
Expand Down Expand Up @@ -135,7 +134,7 @@ class ThumbnailFetcher internal constructor(
val model: Any = when {
svgFetch -> SvgImage(context, uri)
tiffFetch -> TiffImage(context, uri, pageId)
multiTrackFetch -> MultiTrackImage(context, uri, pageId)
multiPageFetch -> MultiPageImage(context, uri, mimeType, pageId)
else -> StorageUtils.getGlideSafeUri(context, uri, mimeType)
}
Glide.with(context)
Expand Down
Loading

0 comments on commit 32fff62

Please sign in to comment.