#976 mpf: use primary rotation for pages
This commit is contained in:
parent
bf8906a9f1
commit
e777c35b1e
5 changed files with 71 additions and 14 deletions
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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#';
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in a new issue