#838 viewer: mpf multipage retrieval / thumbnails

This commit is contained in:
Thibault Deckers 2023-12-10 01:40:02 +01:00
parent 82b4c8aaa1
commit 16aa283425
11 changed files with 217 additions and 121 deletions

View file

@ -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,27 +150,25 @@ 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 ->
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.getJpegMultiPictureFormatBaseOffset(context, uri, sizeBytes)
val baseOffset = MultiPage.getJpegMpfBaseOffset(context, uri)
if (baseOffset != null) {
dataOffset += baseOffset
}
@ -183,16 +180,8 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
return
}
}
}
} 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)
}
}
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) {

View file

@ -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
}

View file

@ -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()

View file

@ -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)

View file

@ -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 {

View file

@ -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 {

View file

@ -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

View file

@ -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) {

View file

@ -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

View file

@ -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,

View file

@ -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: