#976 mpf: use primary rotation for pages

This commit is contained in:
Thibault Deckers 2024-04-15 20:52:28 +02:00
parent bf8906a9f1
commit e777c35b1e
5 changed files with 71 additions and 14 deletions

View file

@ -153,13 +153,13 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
} }
val pageIndex = id - 1 val pageIndex = id - 1
val mpEntries = MultiPage.getJpegMpfEntries(context, uri) val mpEntries = MultiPage.getJpegMpfEntries(context, uri, sizeBytes)
if (mpEntries != null && pageIndex < mpEntries.size) { if (mpEntries != null && pageIndex < mpEntries.size) {
val mpEntry = mpEntries[pageIndex] val mpEntry = mpEntries[pageIndex]
mpEntry.mimeType?.let { embedMimeType -> mpEntry.mimeType?.let { embedMimeType ->
var dataOffset = mpEntry.dataOffset var dataOffset = mpEntry.dataOffset
if (dataOffset > 0) { if (dataOffset > 0) {
val baseOffset = MultiPage.getJpegMpfBaseOffset(context, uri) val baseOffset = MultiPage.getJpegMpfBaseOffset(context, uri, sizeBytes)
if (baseOffset != null) { if (baseOffset != null) {
dataOffset += baseOffset dataOffset += baseOffset
} }

View file

@ -1004,7 +1004,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
} else { } else {
when (mimeType) { when (mimeType) {
MimeTypes.HEIC, MimeTypes.HEIF -> MultiPage.getHeicTracks(context, uri) 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) MimeTypes.TIFF -> MultiPage.getTiffPages(context, uri)
else -> null else -> null
} }

View file

@ -9,10 +9,15 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import androidx.exifinterface.media.ExifInterface
import com.adobe.internal.xmp.XMPMeta import com.adobe.internal.xmp.XMPMeta
import com.drew.imaging.jpeg.JpegSegmentType 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 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
import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeInt
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntry import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntry
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory
import deckers.thibault.aves.metadata.xmp.GoogleXMP 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.model.FieldMap
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.canReadWithMetadataExtractor
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.indexOfBytes import deckers.thibault.aves.utils.indexOfBytes
import org.beyka.tiffbitmapfactory.TiffBitmapFactory import org.beyka.tiffbitmapfactory.TiffBitmapFactory
@ -83,13 +89,58 @@ object MultiPage {
return tracks 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)]` // 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 app2Marker = JpegSegmentType.APP2.byteValue
val mpfMarker = "MPF".toByteArray() + 0x00 val mpfMarker = "MPF".toByteArray() + 0x00
try { try {
Metadata.openSafeInputStream(context, uri, MimeTypes.JPEG, null)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
var offset = 0 var offset = 0
while (true) { while (true) {
do { do {
@ -113,9 +164,10 @@ object MultiPage {
return null return null
} }
fun getJpegMpfEntries(context: Context, uri: Uri): List<MpEntry>? { fun getJpegMpfEntries(context: Context, uri: Uri, sizeBytes: Long?): List<MpEntry>? {
val mimeType = MimeTypes.JPEG
try { try {
Metadata.openSafeInputStream(context, uri, MimeTypes.JPEG, null)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = Helper.safeRead(input) val metadata = Helper.safeRead(input)
return metadata.getDirectoriesOfType(MpEntryDirectory::class.java).map { it.entry } return metadata.getDirectoriesOfType(MpEntryDirectory::class.java).map { it.entry }
} }
@ -129,10 +181,12 @@ object MultiPage {
return null return null
} }
fun getJpegMpfPages(context: Context, uri: Uri): ArrayList<FieldMap> { fun getJpegMpfPages(context: Context, uri: Uri, sizeBytes: Long): ArrayList<FieldMap> {
val primaryRotation = getJpegMpfPrimaryRotation(context, uri, sizeBytes)
val pages = ArrayList<FieldMap>() val pages = ArrayList<FieldMap>()
val baseOffset = getJpegMpfBaseOffset(context, uri) val baseOffset = getJpegMpfBaseOffset(context, uri, sizeBytes)
val mpEntries = getJpegMpfEntries(context, uri) val mpEntries = getJpegMpfEntries(context, uri, sizeBytes)
if (mpEntries != null && baseOffset != null) { if (mpEntries != null && baseOffset != null) {
for ((pageIndex, mpEntry) in mpEntries.withIndex()) { for ((pageIndex, mpEntry) in mpEntries.withIndex()) {
mpEntry.mimeType?.let { embedMimeType -> mpEntry.mimeType?.let { embedMimeType ->
@ -140,8 +194,7 @@ object MultiPage {
KEY_PAGE to pageIndex, KEY_PAGE to pageIndex,
KEY_MIME_TYPE to embedMimeType, KEY_MIME_TYPE to embedMimeType,
KEY_IS_DEFAULT to (pageIndex == 0), KEY_IS_DEFAULT to (pageIndex == 0),
// TODO TLAD [MPF] page[KEY_ROTATION_DEGREES] = same as primary KEY_ROTATION_DEGREES to primaryRotation,
KEY_ROTATION_DEGREES to 0,
) )
var dataOffset = mpEntry.dataOffset var dataOffset = mpEntry.dataOffset
@ -167,12 +220,12 @@ object MultiPage {
} }
fun getJpegMpfBitmap(context: Context, uri: Uri, pageIndex: Int): Bitmap? { 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) { if (mpEntries != null && pageIndex < mpEntries.size) {
val mpEntry = mpEntries[pageIndex] val mpEntry = mpEntries[pageIndex]
var dataOffset = mpEntry.dataOffset var dataOffset = mpEntry.dataOffset
if (dataOffset > 0) { if (dataOffset > 0) {
val baseOffset = getJpegMpfBaseOffset(context, uri) val baseOffset = getJpegMpfBaseOffset(context, uri, null)
if (baseOffset != null) { if (baseOffset != null) {
dataOffset += baseOffset dataOffset += baseOffset
} }

View file

@ -2,6 +2,8 @@ class XmpNamespaces {
static const acdsee = 'http://ns.acdsee.com/iptc/1.0/'; static const acdsee = 'http://ns.acdsee.com/iptc/1.0/';
static const adsmlat = 'http://adsml.org/xmlns/'; static const adsmlat = 'http://adsml.org/xmlns/';
static const appleDesktop = 'http://ns.apple.com/namespace/1.0/'; 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 avm = 'http://www.communicatingastronomy.org/avm/1.0/';
static const camera = 'http://pix4d.com/camera/1.0/'; static const camera = 'http://pix4d.com/camera/1.0/';
static const cc = 'http://creativecommons.org/ns#'; static const cc = 'http://creativecommons.org/ns#';

View file

@ -8,6 +8,8 @@ class XmpNamespaceView {
XmpNamespaces.exifAux: 'Exif Aux', XmpNamespaces.exifAux: 'Exif Aux',
XmpNamespaces.avm: 'Astronomy Visualization', XmpNamespaces.avm: 'Astronomy Visualization',
XmpNamespaces.appleDesktop: 'Apple Desktop', XmpNamespaces.appleDesktop: 'Apple Desktop',
XmpNamespaces.appleHDRGainMap: 'Apple HDR Gain Map',
XmpNamespaces.applePixelDataInfo: 'Apple Pixel Data Info',
XmpNamespaces.camera: 'Pix4D Camera', XmpNamespaces.camera: 'Pix4D Camera',
XmpNamespaces.cc: 'Creative Commons', XmpNamespaces.cc: 'Creative Commons',
XmpNamespaces.crd: 'Camera Raw Defaults', XmpNamespaces.crd: 'Camera Raw Defaults',