diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt index 8cafc76b3..e46d31a46 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt @@ -153,13 +153,13 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { } val pageIndex = id - 1 - val mpEntries = MultiPage.getJpegMpfEntries(context, uri) + val mpEntries = MultiPage.getJpegMpfEntries(context, uri, sizeBytes) 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) + val baseOffset = MultiPage.getJpegMpfBaseOffset(context, uri, sizeBytes) if (baseOffset != null) { dataOffset += baseOffset } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt index 91170c56b..0ef28ec13 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt @@ -1004,7 +1004,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { } else { when (mimeType) { MimeTypes.HEIC, MimeTypes.HEIF -> MultiPage.getHeicTracks(context, uri) - MimeTypes.JPEG -> MultiPage.getJpegMpfPages(context, uri) + MimeTypes.JPEG -> MultiPage.getJpegMpfPages(context, uri, sizeBytes) MimeTypes.TIFF -> MultiPage.getTiffPages(context, uri) else -> null } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt index 65876c72c..2a7d679ac 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt @@ -9,10 +9,15 @@ import android.net.Uri import android.os.Build import android.os.ParcelFileDescriptor import android.util.Log +import androidx.exifinterface.media.ExifInterface import com.adobe.internal.xmp.XMPMeta import com.drew.imaging.jpeg.JpegSegmentType +import com.drew.metadata.exif.ExifDirectoryBase +import com.drew.metadata.exif.ExifIFD0Directory import com.drew.metadata.xmp.XmpDirectory +import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeInt import deckers.thibault.aves.metadata.metadataextractor.Helper +import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeInt import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntry import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory import deckers.thibault.aves.metadata.xmp.GoogleXMP @@ -20,6 +25,7 @@ import deckers.thibault.aves.metadata.xmp.XMP import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes +import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.indexOfBytes import org.beyka.tiffbitmapfactory.TiffBitmapFactory @@ -83,13 +89,58 @@ object MultiPage { return tracks } + private fun getJpegMpfPrimaryRotation(context: Context, uri: Uri, sizeBytes: Long): Int { + val mimeType = MimeTypes.JPEG + var rotationDegrees = 0 + + var foundExif = false + if (canReadWithMetadataExtractor(mimeType)) { + try { + Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> + val metadata = Helper.safeRead(input) + foundExif = metadata.directories.any { it is ExifDirectoryBase && it.tagCount > 0 } + for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) { + dir.getSafeInt(ExifDirectoryBase.TAG_ORIENTATION) { + rotationDegrees = Metadata.getRotationDegreesForExifCode(it) + } + } + } + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } catch (e: NoClassDefFoundError) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } + } + + if (!foundExif) { + // fallback to read EXIF via ExifInterface + try { + Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> + val exif = ExifInterface(input) + exif.getSafeInt(ExifInterface.TAG_ORIENTATION, acceptZero = false) { + rotationDegrees = exif.rotationDegrees + } + } + } catch (e: Exception) { + // ExifInterface initialization can fail with a RuntimeException + // caused by an internal MediaMetadataRetriever failure + Log.w(LOG_TAG, "failed to get metadata by ExifInterface for mimeType=$mimeType uri=$uri", e) + } + } + + return rotationDegrees + } + // starts after `[APP2 marker (1 byte)] [segment size (2 bytes)] [MPF marker (4 bytes)]` - fun getJpegMpfBaseOffset(context: Context, uri: Uri): Int? { + fun getJpegMpfBaseOffset(context: Context, uri: Uri, sizeBytes: Long?): Int? { + val mimeType = MimeTypes.JPEG val app2Marker = JpegSegmentType.APP2.byteValue val mpfMarker = "MPF".toByteArray() + 0x00 try { - Metadata.openSafeInputStream(context, uri, MimeTypes.JPEG, null)?.use { input -> + Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> var offset = 0 while (true) { do { @@ -113,9 +164,10 @@ object MultiPage { return null } - fun getJpegMpfEntries(context: Context, uri: Uri): List? { + fun getJpegMpfEntries(context: Context, uri: Uri, sizeBytes: Long?): List? { + val mimeType = MimeTypes.JPEG try { - Metadata.openSafeInputStream(context, uri, MimeTypes.JPEG, null)?.use { input -> + Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> val metadata = Helper.safeRead(input) return metadata.getDirectoriesOfType(MpEntryDirectory::class.java).map { it.entry } } @@ -129,10 +181,12 @@ object MultiPage { return null } - fun getJpegMpfPages(context: Context, uri: Uri): ArrayList { + fun getJpegMpfPages(context: Context, uri: Uri, sizeBytes: Long): ArrayList { + val primaryRotation = getJpegMpfPrimaryRotation(context, uri, sizeBytes) + val pages = ArrayList() - val baseOffset = getJpegMpfBaseOffset(context, uri) - val mpEntries = getJpegMpfEntries(context, uri) + val baseOffset = getJpegMpfBaseOffset(context, uri, sizeBytes) + val mpEntries = getJpegMpfEntries(context, uri, sizeBytes) if (mpEntries != null && baseOffset != null) { for ((pageIndex, mpEntry) in mpEntries.withIndex()) { mpEntry.mimeType?.let { embedMimeType -> @@ -140,8 +194,7 @@ object MultiPage { KEY_PAGE to pageIndex, KEY_MIME_TYPE to embedMimeType, KEY_IS_DEFAULT to (pageIndex == 0), - // TODO TLAD [MPF] page[KEY_ROTATION_DEGREES] = same as primary - KEY_ROTATION_DEGREES to 0, + KEY_ROTATION_DEGREES to primaryRotation, ) var dataOffset = mpEntry.dataOffset @@ -167,12 +220,12 @@ object MultiPage { } fun getJpegMpfBitmap(context: Context, uri: Uri, pageIndex: Int): Bitmap? { - val mpEntries = getJpegMpfEntries(context, uri) + val mpEntries = getJpegMpfEntries(context, uri, null) if (mpEntries != null && pageIndex < mpEntries.size) { val mpEntry = mpEntries[pageIndex] var dataOffset = mpEntry.dataOffset if (dataOffset > 0) { - val baseOffset = getJpegMpfBaseOffset(context, uri) + val baseOffset = getJpegMpfBaseOffset(context, uri, null) if (baseOffset != null) { dataOffset += baseOffset } diff --git a/lib/ref/metadata/xmp.dart b/lib/ref/metadata/xmp.dart index 5a7ae5942..d4affa709 100644 --- a/lib/ref/metadata/xmp.dart +++ b/lib/ref/metadata/xmp.dart @@ -2,6 +2,8 @@ class XmpNamespaces { static const acdsee = 'http://ns.acdsee.com/iptc/1.0/'; static const adsmlat = 'http://adsml.org/xmlns/'; static const appleDesktop = 'http://ns.apple.com/namespace/1.0/'; + static const appleHDRGainMap = 'http://ns.apple.com/HDRGainMap/1.0/'; + static const applePixelDataInfo = 'http://ns.apple.com/pixeldatainfo/1.0/'; static const avm = 'http://www.communicatingastronomy.org/avm/1.0/'; static const camera = 'http://pix4d.com/camera/1.0/'; static const cc = 'http://creativecommons.org/ns#'; diff --git a/lib/view/src/xmp.dart b/lib/view/src/xmp.dart index cbac7e646..5c8a1a61e 100644 --- a/lib/view/src/xmp.dart +++ b/lib/view/src/xmp.dart @@ -8,6 +8,8 @@ class XmpNamespaceView { XmpNamespaces.exifAux: 'Exif Aux', XmpNamespaces.avm: 'Astronomy Visualization', XmpNamespaces.appleDesktop: 'Apple Desktop', + XmpNamespaces.appleHDRGainMap: 'Apple HDR Gain Map', + XmpNamespaces.applePixelDataInfo: 'Apple Pixel Data Info', XmpNamespaces.camera: 'Pix4D Camera', XmpNamespaces.cc: 'Creative Commons', XmpNamespaces.crd: 'Camera Raw Defaults',