#838 viewer: mpf multipage retrieval / thumbnails
This commit is contained in:
parent
82b4c8aaa1
commit
16aa283425
11 changed files with 217 additions and 121 deletions
|
@ -20,7 +20,6 @@ import deckers.thibault.aves.metadata.XMP.getSafeStructField
|
|||
import deckers.thibault.aves.metadata.XMPPropName
|
||||
import deckers.thibault.aves.metadata.metadataextractor.Helper
|
||||
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntry
|
||||
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
import deckers.thibault.aves.model.provider.ContentImageProvider
|
||||
import deckers.thibault.aves.model.provider.ImageProvider
|
||||
|
@ -51,7 +50,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) }
|
||||
"extractJpegMultiPictureFormat" -> ioScope.launch { safe(call, result, ::extractJpegMultiPictureFormat) }
|
||||
"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) }
|
||||
|
@ -151,48 +150,38 @@ 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 extractJpegMultiPictureFormat(call: MethodCall, result: MethodChannel.Result) {
|
||||
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("extractJpegMultiPictureFormat-args", "missing arguments", null)
|
||||
result.error("extractJpegMpfItem-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
if (canReadWithMetadataExtractor(mimeType)) {
|
||||
try {
|
||||
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
|
||||
val metadata = Helper.safeRead(input)
|
||||
metadata.getDirectoriesOfType(MpEntryDirectory::class.java).first { it.id == id }?.let { dir ->
|
||||
val mpEntry = dir.entry
|
||||
MpEntry.getMimeType(dir.entry.format)?.let { embedMimeType ->
|
||||
var dataOffset = mpEntry.dataOffset
|
||||
if (dataOffset > 0) {
|
||||
val baseOffset = MultiPage.getJpegMultiPictureFormatBaseOffset(context, uri, sizeBytes)
|
||||
if (baseOffset != null) {
|
||||
dataOffset += baseOffset
|
||||
}
|
||||
}
|
||||
StorageUtils.openInputStream(context, uri)?.let { input ->
|
||||
input.skip(dataOffset)
|
||||
copyEmbeddedBytes(result, embedMimeType, displayName, input, mpEntry.size)
|
||||
}
|
||||
return
|
||||
}
|
||||
val pageIndex = id - 1
|
||||
val mpEntries = MultiPage.getJpegMpfEntries(context, uri)
|
||||
if (mpEntries != null && pageIndex < mpEntries.size) {
|
||||
val mpEntry = mpEntries[pageIndex]
|
||||
MpEntry.getMimeType(mpEntry.format)?.let { embedMimeType ->
|
||||
var dataOffset = mpEntry.dataOffset
|
||||
if (dataOffset > 0) {
|
||||
val baseOffset = MultiPage.getJpegMpfBaseOffset(context, uri)
|
||||
if (baseOffset != null) {
|
||||
dataOffset += baseOffset
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to extract file from MPF", e)
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
Log.w(LOG_TAG, "failed to extract file from MPF", e)
|
||||
} catch (e: AssertionError) {
|
||||
Log.w(LOG_TAG, "failed to extract file from MPF", e)
|
||||
StorageUtils.openInputStream(context, uri)?.let { input ->
|
||||
input.skip(dataOffset)
|
||||
copyEmbeddedBytes(result, embedMimeType, displayName, input, mpEntry.size)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
result.error("extractJpegMultiPictureFormat-empty", "failed to extract file index=$id from MPF at uri=$uri", null)
|
||||
|
||||
result.error("extractJpegMpfItem-empty", "failed to extract file index=$id from MPF at uri=$uri", null)
|
||||
}
|
||||
|
||||
private fun extractMotionPhotoImage(call: MethodCall, result: MethodChannel.Result) {
|
||||
|
|
|
@ -933,10 +933,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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
|
|
@ -12,7 +12,7 @@ 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
|
||||
|
@ -20,7 +20,6 @@ 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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
|
|
@ -9,7 +9,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.decoder.TiffImage
|
||||
import deckers.thibault.aves.decoder.VideoThumbnail
|
||||
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
|
||||
|
@ -18,7 +18,6 @@ import deckers.thibault.aves.utils.LogUtils
|
|||
import deckers.thibault.aves.utils.MemoryUtils
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
import deckers.thibault.aves.utils.MimeTypes.canDecodeWithFlutter
|
||||
import deckers.thibault.aves.utils.MimeTypes.isHeic
|
||||
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
||||
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
|
||||
import deckers.thibault.aves.utils.StorageUtils
|
||||
|
@ -131,8 +130,8 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
|
|||
rotationDegrees: Int,
|
||||
isFlipped: Boolean,
|
||||
) {
|
||||
val model: Any = if (isHeic(mimeType) && pageId != null) {
|
||||
MultiTrackImage(context, uri, pageId)
|
||||
val model: Any = if (pageId != null && MultiPageImage.isSupported(mimeType)) {
|
||||
MultiPageImage(context, uri, mimeType, pageId)
|
||||
} else if (mimeType == MimeTypes.TIFF) {
|
||||
TiffImage(context, uri, pageId)
|
||||
} else {
|
||||
|
|
|
@ -17,32 +17,38 @@ import com.bumptech.glide.load.model.ModelLoaderFactory
|
|||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||
import com.bumptech.glide.module.LibraryGlideModule
|
||||
import com.bumptech.glide.signature.ObjectKey
|
||||
import deckers.thibault.aves.metadata.MultiPage
|
||||
import deckers.thibault.aves.metadata.MultiTrackMedia
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
|
||||
@GlideModule
|
||||
class MultiTrackImageGlideModule : LibraryGlideModule() {
|
||||
class MultiPageImageGlideModule : LibraryGlideModule() {
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
registry.append(MultiTrackImage::class.java, Bitmap::class.java, MultiTrackThumbnailLoader.Factory())
|
||||
registry.append(MultiPageImage::class.java, Bitmap::class.java, MultiPageThumbnailLoader.Factory())
|
||||
}
|
||||
}
|
||||
|
||||
class MultiTrackImage(val context: Context, val uri: Uri, val trackIndex: Int?)
|
||||
class MultiPageImage(val context: Context, val uri: Uri, val mimeType: String, val pageId: Int?) {
|
||||
companion object {
|
||||
fun isSupported(mimeType: String) = MimeTypes.isHeic(mimeType) || mimeType == MimeTypes.JPEG
|
||||
}
|
||||
}
|
||||
|
||||
internal class MultiTrackThumbnailLoader : ModelLoader<MultiTrackImage, Bitmap> {
|
||||
override fun buildLoadData(model: MultiTrackImage, width: Int, height: Int, options: Options): ModelLoader.LoadData<Bitmap> {
|
||||
return ModelLoader.LoadData(ObjectKey(model.uri), MultiTrackImageFetcher(model, width, height))
|
||||
internal class MultiPageThumbnailLoader : ModelLoader<MultiPageImage, Bitmap> {
|
||||
override fun buildLoadData(model: MultiPageImage, width: Int, height: Int, options: Options): ModelLoader.LoadData<Bitmap> {
|
||||
return ModelLoader.LoadData(ObjectKey(model.uri), MultiPageImageFetcher(model, width, height))
|
||||
}
|
||||
|
||||
override fun handles(model: MultiTrackImage): Boolean = true
|
||||
override fun handles(model: MultiPageImage): Boolean = true
|
||||
|
||||
internal class Factory : ModelLoaderFactory<MultiTrackImage, Bitmap> {
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MultiTrackImage, Bitmap> = MultiTrackThumbnailLoader()
|
||||
internal class Factory : ModelLoaderFactory<MultiPageImage, Bitmap> {
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MultiPageImage, Bitmap> = MultiPageThumbnailLoader()
|
||||
|
||||
override fun teardown() {}
|
||||
}
|
||||
}
|
||||
|
||||
internal class MultiTrackImageFetcher(val model: MultiTrackImage, val width: Int, val height: Int) : DataFetcher<Bitmap> {
|
||||
internal class MultiPageImageFetcher(val model: MultiPageImage, val width: Int, val height: Int) : DataFetcher<Bitmap> {
|
||||
override fun loadData(priority: Priority, callback: DataCallback<in Bitmap>) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
callback.onLoadFailed(Exception("unsupported Android version"))
|
||||
|
@ -51,9 +57,17 @@ internal class MultiTrackImageFetcher(val model: MultiTrackImage, val width: Int
|
|||
|
||||
val context = model.context
|
||||
val uri = model.uri
|
||||
val trackIndex = model.trackIndex
|
||||
val mimeType = model.mimeType
|
||||
|
||||
var bitmap: Bitmap? = null
|
||||
if (MimeTypes.isHeic(mimeType)) {
|
||||
val trackIndex = model.pageId
|
||||
bitmap = MultiTrackMedia.getImage(context, uri, trackIndex)
|
||||
} else if (mimeType == MimeTypes.JPEG) {
|
||||
val pageIndex = model.pageId ?: 0
|
||||
bitmap = MultiPage.getJpegMpfBitmap(context, uri, pageIndex)
|
||||
}
|
||||
|
||||
val bitmap = MultiTrackMedia.getImage(context, uri, trackIndex)
|
||||
if (bitmap == null) {
|
||||
callback.onLoadFailed(Exception("null bitmap"))
|
||||
} else {
|
|
@ -1,6 +1,8 @@
|
|||
package deckers.thibault.aves.metadata
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.media.MediaExtractor
|
||||
import android.media.MediaFormat
|
||||
import android.net.Uri
|
||||
|
@ -15,9 +17,12 @@ import deckers.thibault.aves.metadata.XMP.doesPropExist
|
|||
import deckers.thibault.aves.metadata.XMP.getSafeLong
|
||||
import deckers.thibault.aves.metadata.XMP.getSafeStructField
|
||||
import deckers.thibault.aves.metadata.metadataextractor.Helper
|
||||
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntry
|
||||
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
import deckers.thibault.aves.utils.StorageUtils
|
||||
import deckers.thibault.aves.utils.indexOfBytes
|
||||
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
|
||||
import java.io.DataInputStream
|
||||
|
@ -48,13 +53,13 @@ object MultiPage {
|
|||
val tracks = ArrayList<FieldMap>()
|
||||
val extractor = MediaExtractor()
|
||||
extractor.setDataSource(context, uri, null)
|
||||
for (i in 0 until extractor.trackCount) {
|
||||
for (pageIndex in 0 until extractor.trackCount) {
|
||||
try {
|
||||
val format = extractor.getTrackFormat(i)
|
||||
val format = extractor.getTrackFormat(pageIndex)
|
||||
format.getString(MediaFormat.KEY_MIME)?.let { mime ->
|
||||
val trackMime = if (mime == MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC) MimeTypes.HEIC else mime
|
||||
val track: FieldMap = hashMapOf(
|
||||
KEY_PAGE to i,
|
||||
KEY_PAGE to pageIndex,
|
||||
KEY_MIME_TYPE to trackMime,
|
||||
)
|
||||
|
||||
|
@ -73,13 +78,115 @@ object MultiPage {
|
|||
tracks.add(track)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to get HEIC track information for uri=$uri, track num=$i", e)
|
||||
Log.w(LOG_TAG, "failed to get HEIC track information for uri=$uri, pageIndex=$pageIndex", e)
|
||||
}
|
||||
}
|
||||
extractor.release()
|
||||
return tracks
|
||||
}
|
||||
|
||||
// starts after `[APP2 marker (1 byte)] [segment size (2 bytes)] [MPF marker (4 bytes)]`
|
||||
fun getJpegMpfBaseOffset(context: Context, uri: Uri): Int? {
|
||||
val app2Marker = JpegSegmentType.APP2.byteValue
|
||||
val mpfMarker = "MPF".toByteArray() + 0x00
|
||||
|
||||
try {
|
||||
Metadata.openSafeInputStream(context, uri, MimeTypes.JPEG, null)?.use { input ->
|
||||
var offset = 0
|
||||
while (true) {
|
||||
do {
|
||||
val b = input.read().toByte()
|
||||
offset++
|
||||
} while (b != app2Marker)
|
||||
// skip 2 bytes for segment size
|
||||
input.skip(2)
|
||||
offset += 2
|
||||
val marker = ByteArray(4)
|
||||
input.read(marker, 0, marker.size)
|
||||
offset += 4
|
||||
if (marker.contentEquals(mpfMarker)) {
|
||||
return offset
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to get MPF base offset from uri=$uri", e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getJpegMpfEntries(context: Context, uri: Uri): List<MpEntry>? {
|
||||
try {
|
||||
Metadata.openSafeInputStream(context, uri, MimeTypes.JPEG, null)?.use { input ->
|
||||
val metadata = Helper.safeRead(input)
|
||||
return metadata.getDirectoriesOfType(MpEntryDirectory::class.java).map { it.entry }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to find MPF entries", e)
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
Log.w(LOG_TAG, "failed to find MPF entries", e)
|
||||
} catch (e: AssertionError) {
|
||||
Log.w(LOG_TAG, "failed to find MPF entries", e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getJpegMpfPages(context: Context, uri: Uri): ArrayList<FieldMap> {
|
||||
val pages = ArrayList<FieldMap>()
|
||||
val baseOffset = getJpegMpfBaseOffset(context, uri)
|
||||
val mpEntries = getJpegMpfEntries(context, uri)
|
||||
if (mpEntries != null && baseOffset != null) {
|
||||
for ((pageIndex, mpEntry) in mpEntries.withIndex()) {
|
||||
MpEntry.getMimeType(mpEntry.format)?.let { embedMimeType ->
|
||||
val page = hashMapOf<String, Any?>(
|
||||
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,
|
||||
)
|
||||
|
||||
var dataOffset = mpEntry.dataOffset
|
||||
if (dataOffset > 0) {
|
||||
dataOffset += baseOffset
|
||||
}
|
||||
StorageUtils.openInputStream(context, uri)?.let { input ->
|
||||
input.skip(dataOffset)
|
||||
val options = BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
}
|
||||
BitmapFactory.decodeStream(input, null, options)
|
||||
options.outWidth.takeIf { it >= 0 }?.let { page[KEY_WIDTH] = it }
|
||||
options.outHeight.takeIf { it >= 0 }?.let { page[KEY_HEIGHT] = it }
|
||||
|
||||
pages.add(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
fun getJpegMpfBitmap(context: Context, uri: Uri, pageIndex: Int): Bitmap? {
|
||||
val mpEntries = getJpegMpfEntries(context, uri)
|
||||
if (mpEntries != null && pageIndex < mpEntries.size) {
|
||||
val mpEntry = mpEntries[pageIndex]
|
||||
var dataOffset = mpEntry.dataOffset
|
||||
if (dataOffset > 0) {
|
||||
val baseOffset = getJpegMpfBaseOffset(context, uri)
|
||||
if (baseOffset != null) {
|
||||
dataOffset += baseOffset
|
||||
}
|
||||
}
|
||||
StorageUtils.openInputStream(context, uri)?.let { input ->
|
||||
input.skip(dataOffset)
|
||||
return BitmapFactory.decodeStream(input)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getMotionPhotoPages(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): ArrayList<FieldMap> {
|
||||
fun MediaFormat.getSafeInt(key: String, save: (value: Int) -> Unit) {
|
||||
if (this.containsKey(key)) save(this.getInteger(key))
|
||||
|
@ -89,7 +196,7 @@ object MultiPage {
|
|||
if (this.containsKey(key)) save(this.getLong(key))
|
||||
}
|
||||
|
||||
val tracks = ArrayList<FieldMap>()
|
||||
val pages = ArrayList<FieldMap>()
|
||||
val extractor = MediaExtractor()
|
||||
var pfd: ParcelFileDescriptor? = null
|
||||
try {
|
||||
|
@ -99,10 +206,10 @@ object MultiPage {
|
|||
pfd?.fileDescriptor?.let { fd ->
|
||||
extractor.setDataSource(fd, videoStartOffset, videoSizeBytes)
|
||||
// set the original image as the first and default track
|
||||
var trackCount = 0
|
||||
tracks.add(
|
||||
var pageIndex = 0
|
||||
pages.add(
|
||||
hashMapOf(
|
||||
KEY_PAGE to trackCount++,
|
||||
KEY_PAGE to pageIndex++,
|
||||
KEY_MIME_TYPE to mimeType,
|
||||
KEY_IS_DEFAULT to true,
|
||||
)
|
||||
|
@ -115,18 +222,18 @@ object MultiPage {
|
|||
val format = extractor.getTrackFormat(trackIndex)
|
||||
format.getString(MediaFormat.KEY_MIME)?.let { mime ->
|
||||
if (MimeTypes.isVideo(mime)) {
|
||||
val track: FieldMap = hashMapOf(
|
||||
KEY_PAGE to trackCount++,
|
||||
val page: FieldMap = hashMapOf(
|
||||
KEY_PAGE to pageIndex++,
|
||||
KEY_MIME_TYPE to MimeTypes.MP4,
|
||||
KEY_IS_DEFAULT to false,
|
||||
)
|
||||
format.getSafeInt(MediaFormat.KEY_WIDTH) { track[KEY_WIDTH] = it }
|
||||
format.getSafeInt(MediaFormat.KEY_HEIGHT) { track[KEY_HEIGHT] = it }
|
||||
format.getSafeInt(MediaFormat.KEY_WIDTH) { page[KEY_WIDTH] = it }
|
||||
format.getSafeInt(MediaFormat.KEY_HEIGHT) { page[KEY_HEIGHT] = it }
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
format.getSafeInt(MediaFormat.KEY_ROTATION) { track[KEY_ROTATION_DEGREES] = it }
|
||||
format.getSafeInt(MediaFormat.KEY_ROTATION) { page[KEY_ROTATION_DEGREES] = it }
|
||||
}
|
||||
format.getSafeLong(MediaFormat.KEY_DURATION) { track[KEY_DURATION] = it / 1000 }
|
||||
tracks.add(track)
|
||||
format.getSafeLong(MediaFormat.KEY_DURATION) { page[KEY_DURATION] = it / 1000 }
|
||||
pages.add(page)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -141,7 +248,7 @@ object MultiPage {
|
|||
extractor.release()
|
||||
pfd?.close()
|
||||
}
|
||||
return tracks
|
||||
return pages
|
||||
}
|
||||
|
||||
fun getMotionPhotoOffset(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): Long? {
|
||||
|
@ -204,40 +311,10 @@ object MultiPage {
|
|||
return offsetFromEnd
|
||||
}
|
||||
|
||||
// starts after `[APP2 marker (1 byte)] [segment size (2 bytes)] [MPF marker (4 bytes)]`
|
||||
fun getJpegMultiPictureFormatBaseOffset(context: Context, uri: Uri, sizeBytes: Long): Int? {
|
||||
val app2Marker = JpegSegmentType.APP2.byteValue
|
||||
val mpfMarker = "MPF".toByteArray() + 0x00
|
||||
|
||||
try {
|
||||
Metadata.openSafeInputStream(context, uri, MimeTypes.JPEG, sizeBytes)?.use { input ->
|
||||
var offset = 0
|
||||
while (true) {
|
||||
do {
|
||||
val b = input.read().toByte()
|
||||
offset++
|
||||
} while (b != app2Marker)
|
||||
// skip 2 bytes for segment size
|
||||
input.skip(2)
|
||||
offset += 2
|
||||
val marker = ByteArray(4)
|
||||
input.read(marker, 0, marker.size)
|
||||
offset += 4
|
||||
if (marker.contentEquals(mpfMarker)) {
|
||||
return offset
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to get MPF base offset from uri=$uri", e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getTiffPages(context: Context, uri: Uri): ArrayList<FieldMap> {
|
||||
fun toMap(page: Int, options: TiffBitmapFactory.Options): FieldMap {
|
||||
fun toMap(pageIndex: Int, options: TiffBitmapFactory.Options): FieldMap {
|
||||
return hashMapOf(
|
||||
KEY_PAGE to page,
|
||||
KEY_PAGE to pageIndex,
|
||||
KEY_MIME_TYPE to MimeTypes.TIFF,
|
||||
KEY_WIDTH to options.outWidth,
|
||||
KEY_HEIGHT to options.outHeight,
|
||||
|
@ -248,8 +325,8 @@ object MultiPage {
|
|||
getTiffPageInfo(context, uri, 0)?.let { first ->
|
||||
pages.add(toMap(0, first))
|
||||
val pageCount = first.outDirectoryCount
|
||||
for (i in 1 until pageCount) {
|
||||
getTiffPageInfo(context, uri, i)?.let { pages.add(toMap(i, it)) }
|
||||
for (pageIndex in 1 until pageCount) {
|
||||
getTiffPageInfo(context, uri, pageIndex)?.let { pages.add(toMap(pageIndex, it)) }
|
||||
}
|
||||
}
|
||||
return pages
|
||||
|
|
|
@ -19,25 +19,36 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|||
import com.bumptech.glide.request.FutureTarget
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.commonsware.cwac.document.DocumentFileCompat
|
||||
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.metadata.*
|
||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper
|
||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis
|
||||
import deckers.thibault.aves.metadata.Metadata.TYPE_EXIF
|
||||
import deckers.thibault.aves.metadata.Metadata.TYPE_IPTC
|
||||
import deckers.thibault.aves.metadata.Metadata.TYPE_MP4
|
||||
import deckers.thibault.aves.metadata.Metadata.TYPE_XMP
|
||||
import deckers.thibault.aves.metadata.Mp4ParserHelper
|
||||
import deckers.thibault.aves.metadata.Mp4ParserHelper.updateLocation
|
||||
import deckers.thibault.aves.metadata.Mp4ParserHelper.updateRotation
|
||||
import deckers.thibault.aves.metadata.Mp4ParserHelper.updateXmp
|
||||
import deckers.thibault.aves.metadata.MultiPage
|
||||
import deckers.thibault.aves.metadata.PixyMetaHelper
|
||||
import deckers.thibault.aves.metadata.PixyMetaHelper.extendedXmpDocString
|
||||
import deckers.thibault.aves.metadata.PixyMetaHelper.xmpDocString
|
||||
import deckers.thibault.aves.metadata.XMP
|
||||
import deckers.thibault.aves.metadata.metadataextractor.Helper
|
||||
import deckers.thibault.aves.model.*
|
||||
import deckers.thibault.aves.utils.*
|
||||
import deckers.thibault.aves.model.AvesEntry
|
||||
import deckers.thibault.aves.model.ExifOrientationOp
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
import deckers.thibault.aves.model.NameConflictStrategy
|
||||
import deckers.thibault.aves.model.SourceEntry
|
||||
import deckers.thibault.aves.utils.BitmapUtils
|
||||
import deckers.thibault.aves.utils.BmpWriter
|
||||
import deckers.thibault.aves.utils.FileUtils.transferFrom
|
||||
import deckers.thibault.aves.utils.FileUtils.transferTo
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
import deckers.thibault.aves.utils.MimeTypes.canEditExif
|
||||
import deckers.thibault.aves.utils.MimeTypes.canEditIptc
|
||||
import deckers.thibault.aves.utils.MimeTypes.canEditXmp
|
||||
|
@ -46,13 +57,19 @@ import deckers.thibault.aves.utils.MimeTypes.canReadWithPixyMeta
|
|||
import deckers.thibault.aves.utils.MimeTypes.canRemoveMetadata
|
||||
import deckers.thibault.aves.utils.MimeTypes.extensionFor
|
||||
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
||||
import deckers.thibault.aves.utils.StorageUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import pixy.meta.meta.Metadata
|
||||
import pixy.meta.meta.MetadataType
|
||||
import java.io.*
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.nio.channels.Channels
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
abstract class ImageProvider {
|
||||
|
@ -291,8 +308,8 @@ abstract class ImageProvider {
|
|||
targetHeightPx = sourceEntry.height * targetHeightPx / 100
|
||||
}
|
||||
|
||||
val model: Any = if (MimeTypes.isHeic(sourceMimeType) && pageId != null) {
|
||||
MultiTrackImage(activity, sourceUri, pageId)
|
||||
val model: Any = if (pageId != null && MultiPageImage.isSupported(sourceMimeType)) {
|
||||
MultiPageImage(activity, sourceUri, sourceMimeType, pageId)
|
||||
} else if (sourceMimeType == MimeTypes.TIFF) {
|
||||
TiffImage(activity, sourceUri, pageId)
|
||||
} else if (sourceMimeType == MimeTypes.SVG) {
|
||||
|
|
|
@ -38,7 +38,7 @@ class PlatformAppService implements AppService {
|
|||
'com.sony.playmemories.mobile': {'Imaging Edge Mobile'},
|
||||
'nekox.messenger': {'NekoX'},
|
||||
'org.telegram.messenger': {'Telegram Images', 'Telegram Video'},
|
||||
'com.whatsapp': {'Whatsapp', 'WhatsApp Animated Gifs', 'WhatsApp Images', 'WhatsApp Video'}
|
||||
'com.whatsapp': {'Whatsapp', 'WhatsApp Animated Gifs', 'WhatsApp Documents', 'WhatsApp Images', 'WhatsApp Video'}
|
||||
};
|
||||
|
||||
@override
|
||||
|
|
|
@ -12,7 +12,7 @@ abstract class EmbeddedDataService {
|
|||
|
||||
Future<Map> extractMotionPhotoVideo(AvesEntry entry);
|
||||
|
||||
Future<Map> extractJpegMultiPictureFormat(AvesEntry entry, int index);
|
||||
Future<Map> extractJpegMpfItem(AvesEntry entry, int index);
|
||||
|
||||
Future<Map> extractVideoEmbeddedPicture(AvesEntry entry);
|
||||
|
||||
|
@ -87,9 +87,9 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<Map> extractJpegMultiPictureFormat(AvesEntry entry, int id) async {
|
||||
Future<Map> extractJpegMpfItem(AvesEntry entry, int id) async {
|
||||
try {
|
||||
final result = await _platform.invokeMethod('extractJpegMultiPictureFormat', <String, dynamic>{
|
||||
final result = await _platform.invokeMethod('extractJpegMpfItem', <String, dynamic>{
|
||||
'mimeType': entry.mimeType,
|
||||
'uri': entry.uri,
|
||||
'sizeBytes': entry.sizeBytes,
|
||||
|
|
|
@ -45,7 +45,7 @@ class EmbeddedDataOpener extends StatelessWidget with FeedbackMixin {
|
|||
case EmbeddedDataSource.motionPhotoVideo:
|
||||
fields = await embeddedDataService.extractMotionPhotoVideo(entry);
|
||||
case EmbeddedDataSource.mpf:
|
||||
fields = await embeddedDataService.extractJpegMultiPictureFormat(entry, notification.mpfId!);
|
||||
fields = await embeddedDataService.extractJpegMpfItem(entry, notification.mpfId!);
|
||||
case EmbeddedDataSource.videoCover:
|
||||
fields = await embeddedDataService.extractVideoEmbeddedPicture(entry);
|
||||
case EmbeddedDataSource.xmp:
|
||||
|
|
Loading…
Reference in a new issue