collection: identify multipage TIFF, multitrack HEIC/HEIF
This commit is contained in:
parent
075bb2f07c
commit
cd2811be02
7 changed files with 79 additions and 16 deletions
|
@ -61,6 +61,7 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
|
@ -230,19 +231,24 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val metadataMap = HashMap(getCatalogMetadataByMetadataExtractor(uri, mimeType, path, sizeBytes))
|
val metadataMap = HashMap<String, Any>()
|
||||||
|
getCatalogMetadataByMetadataExtractor(uri, mimeType, path, sizeBytes, metadataMap)
|
||||||
if (isMultimedia(mimeType)) {
|
if (isMultimedia(mimeType)) {
|
||||||
metadataMap.putAll(getMultimediaCatalogMetadataByMediaMetadataRetriever(uri))
|
getMultimediaCatalogMetadataByMediaMetadataRetriever(uri, mimeType, metadataMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
// report success even when empty
|
// report success even when empty
|
||||||
result.success(metadataMap)
|
result.success(metadataMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCatalogMetadataByMetadataExtractor(uri: Uri, mimeType: String, path: String?, sizeBytes: Long?): Map<String, Any> {
|
private fun getCatalogMetadataByMetadataExtractor(
|
||||||
val metadataMap = HashMap<String, Any>()
|
uri: Uri,
|
||||||
|
mimeType: String,
|
||||||
var flags = 0
|
path: String?,
|
||||||
|
sizeBytes: Long?,
|
||||||
|
metadataMap: HashMap<String, Any>,
|
||||||
|
) {
|
||||||
|
var flags = (metadataMap[KEY_FLAGS] ?: 0) as Int
|
||||||
var foundExif = false
|
var foundExif = false
|
||||||
|
|
||||||
if (isSupportedByMetadataExtractor(mimeType)) {
|
if (isSupportedByMetadataExtractor(mimeType)) {
|
||||||
|
@ -390,13 +396,20 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
Log.w(LOG_TAG, "failed to get metadata by ExifInterface for uri=$uri", e)
|
Log.w(LOG_TAG, "failed to get metadata by ExifInterface for uri=$uri", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mimeType == MimeTypes.TIFF && getTiffDirCount(uri) > 1) flags = flags or MASK_IS_MULTIPAGE
|
||||||
|
|
||||||
metadataMap[KEY_FLAGS] = flags
|
metadataMap[KEY_FLAGS] = flags
|
||||||
return metadataMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMultimediaCatalogMetadataByMediaMetadataRetriever(uri: Uri): Map<String, Any> {
|
private fun getMultimediaCatalogMetadataByMediaMetadataRetriever(
|
||||||
val metadataMap = HashMap<String, Any>()
|
uri: Uri,
|
||||||
val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return metadataMap
|
mimeType: String,
|
||||||
|
metadataMap: HashMap<String, Any>,
|
||||||
|
) {
|
||||||
|
val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return
|
||||||
|
|
||||||
|
var flags = (metadataMap[KEY_FLAGS] ?: 0) as Int
|
||||||
try {
|
try {
|
||||||
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { metadataMap[KEY_ROTATION_DEGREES] = it }
|
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { metadataMap[KEY_ROTATION_DEGREES] = it }
|
||||||
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
|
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
|
||||||
|
@ -417,13 +430,20 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mimeType == MimeTypes.HEIC || mimeType == MimeTypes.HEIF) {
|
||||||
|
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS) {
|
||||||
|
if (it > 1) flags = flags or MASK_IS_MULTIPAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataMap[KEY_FLAGS] = flags
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(LOG_TAG, "failed to get catalog metadata by MediaMetadataRetriever for uri=$uri", e)
|
Log.w(LOG_TAG, "failed to get catalog metadata by MediaMetadataRetriever for uri=$uri", e)
|
||||||
} finally {
|
} finally {
|
||||||
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
||||||
retriever.release()
|
retriever.release()
|
||||||
}
|
}
|
||||||
return metadataMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getOverlayMetadata(call: MethodCall, result: MethodChannel.Result) {
|
private fun getOverlayMetadata(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
@ -622,6 +642,25 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
result.error("extractXmpDataProp-empty", "failed to extract file from XMP uri=$uri prop=$dataPropPath", null)
|
result.error("extractXmpDataProp-empty", "failed to extract file from XMP uri=$uri prop=$dataPropPath", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getTiffDirCount(uri: Uri): Int {
|
||||||
|
var dirCount = 1
|
||||||
|
try {
|
||||||
|
val fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd()
|
||||||
|
if (fd == null) {
|
||||||
|
Log.w(LOG_TAG, "failed to get file descriptor for uri=$uri")
|
||||||
|
} else {
|
||||||
|
val options = TiffBitmapFactory.Options().apply {
|
||||||
|
inJustDecodeBounds = true
|
||||||
|
}
|
||||||
|
TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
||||||
|
dirCount = options.outDirectoryCount
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to get TIFF dir count for uri=$uri", e)
|
||||||
|
}
|
||||||
|
return dirCount
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val LOG_TAG = LogUtils.createTag(MetadataHandler::class.java)
|
private val LOG_TAG = LogUtils.createTag(MetadataHandler::class.java)
|
||||||
const val CHANNEL = "deckers.thibault/aves/metadata"
|
const val CHANNEL = "deckers.thibault/aves/metadata"
|
||||||
|
@ -640,6 +679,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
private const val MASK_IS_FLIPPED = 1 shl 1
|
private const val MASK_IS_FLIPPED = 1 shl 1
|
||||||
private const val MASK_IS_GEOTIFF = 1 shl 2
|
private const val MASK_IS_GEOTIFF = 1 shl 2
|
||||||
private const val MASK_IS_360 = 1 shl 3
|
private const val MASK_IS_360 = 1 shl 3
|
||||||
|
private const val MASK_IS_MULTIPAGE = 1 shl 4
|
||||||
private const val XMP_SUBJECTS_SEPARATOR = ";"
|
private const val XMP_SUBJECTS_SEPARATOR = ";"
|
||||||
|
|
||||||
// overlay metadata
|
// overlay metadata
|
||||||
|
|
|
@ -8,8 +8,8 @@ object MimeTypes {
|
||||||
// generic raster
|
// generic raster
|
||||||
private const val BMP = "image/bmp"
|
private const val BMP = "image/bmp"
|
||||||
const val GIF = "image/gif"
|
const val GIF = "image/gif"
|
||||||
private const val HEIC = "image/heic"
|
const val HEIC = "image/heic"
|
||||||
private const val HEIF = "image/heif"
|
const val HEIF = "image/heif"
|
||||||
private const val ICO = "image/x-icon"
|
private const val ICO = "image/x-icon"
|
||||||
private const val JPEG = "image/jpeg"
|
private const val JPEG = "image/jpeg"
|
||||||
private const val PNG = "image/png"
|
private const val PNG = "image/png"
|
||||||
|
|
|
@ -211,6 +211,8 @@ class ImageEntry {
|
||||||
|
|
||||||
bool get is360 => _catalogMetadata?.is360 ?? false;
|
bool get is360 => _catalogMetadata?.is360 ?? false;
|
||||||
|
|
||||||
|
bool get isMultipage => _catalogMetadata?.isMultipage ?? false;
|
||||||
|
|
||||||
bool get canEdit => path != null;
|
bool get canEdit => path != null;
|
||||||
|
|
||||||
bool get canPrint => !isVideo;
|
bool get canPrint => !isVideo;
|
||||||
|
|
|
@ -29,7 +29,7 @@ class DateMetadata {
|
||||||
|
|
||||||
class CatalogMetadata {
|
class CatalogMetadata {
|
||||||
final int contentId, dateMillis;
|
final int contentId, dateMillis;
|
||||||
final bool isAnimated, isGeotiff, is360;
|
final bool isAnimated, isGeotiff, is360, isMultipage;
|
||||||
bool isFlipped;
|
bool isFlipped;
|
||||||
int rotationDegrees;
|
int rotationDegrees;
|
||||||
final String mimeType, xmpSubjects, xmpTitleDescription;
|
final String mimeType, xmpSubjects, xmpTitleDescription;
|
||||||
|
@ -41,6 +41,7 @@ class CatalogMetadata {
|
||||||
static const _isFlippedMask = 1 << 1;
|
static const _isFlippedMask = 1 << 1;
|
||||||
static const _isGeotiffMask = 1 << 2;
|
static const _isGeotiffMask = 1 << 2;
|
||||||
static const _is360Mask = 1 << 3;
|
static const _is360Mask = 1 << 3;
|
||||||
|
static const _isMultipageMask = 1 << 4;
|
||||||
|
|
||||||
CatalogMetadata({
|
CatalogMetadata({
|
||||||
this.contentId,
|
this.contentId,
|
||||||
|
@ -50,6 +51,7 @@ class CatalogMetadata {
|
||||||
this.isFlipped = false,
|
this.isFlipped = false,
|
||||||
this.isGeotiff = false,
|
this.isGeotiff = false,
|
||||||
this.is360 = false,
|
this.is360 = false,
|
||||||
|
this.isMultipage = false,
|
||||||
this.rotationDegrees,
|
this.rotationDegrees,
|
||||||
this.xmpSubjects,
|
this.xmpSubjects,
|
||||||
this.xmpTitleDescription,
|
this.xmpTitleDescription,
|
||||||
|
@ -76,6 +78,7 @@ class CatalogMetadata {
|
||||||
isFlipped: isFlipped,
|
isFlipped: isFlipped,
|
||||||
isGeotiff: isGeotiff,
|
isGeotiff: isGeotiff,
|
||||||
is360: is360,
|
is360: is360,
|
||||||
|
isMultipage: isMultipage,
|
||||||
rotationDegrees: rotationDegrees,
|
rotationDegrees: rotationDegrees,
|
||||||
xmpSubjects: xmpSubjects,
|
xmpSubjects: xmpSubjects,
|
||||||
xmpTitleDescription: xmpTitleDescription,
|
xmpTitleDescription: xmpTitleDescription,
|
||||||
|
@ -94,6 +97,7 @@ class CatalogMetadata {
|
||||||
isFlipped: flags & _isFlippedMask != 0,
|
isFlipped: flags & _isFlippedMask != 0,
|
||||||
isGeotiff: flags & _isGeotiffMask != 0,
|
isGeotiff: flags & _isGeotiffMask != 0,
|
||||||
is360: flags & _is360Mask != 0,
|
is360: flags & _is360Mask != 0,
|
||||||
|
isMultipage: flags & _isMultipageMask != 0,
|
||||||
// `rotationDegrees` should default to `sourceRotationDegrees`, not 0
|
// `rotationDegrees` should default to `sourceRotationDegrees`, not 0
|
||||||
rotationDegrees: map['rotationDegrees'],
|
rotationDegrees: map['rotationDegrees'],
|
||||||
xmpSubjects: map['xmpSubjects'] ?? '',
|
xmpSubjects: map['xmpSubjects'] ?? '',
|
||||||
|
@ -107,7 +111,7 @@ class CatalogMetadata {
|
||||||
'contentId': contentId,
|
'contentId': contentId,
|
||||||
'mimeType': mimeType,
|
'mimeType': mimeType,
|
||||||
'dateMillis': dateMillis,
|
'dateMillis': dateMillis,
|
||||||
'flags': (isAnimated ? _isAnimatedMask : 0) | (isFlipped ? _isFlippedMask : 0) | (isGeotiff ? _isGeotiffMask : 0) | (is360 ? _is360Mask : 0),
|
'flags': (isAnimated ? _isAnimatedMask : 0) | (isFlipped ? _isFlippedMask : 0) | (isGeotiff ? _isGeotiffMask : 0) | (is360 ? _is360Mask : 0) | (isMultipage ? _isMultipageMask : 0),
|
||||||
'rotationDegrees': rotationDegrees,
|
'rotationDegrees': rotationDegrees,
|
||||||
'xmpSubjects': xmpSubjects,
|
'xmpSubjects': xmpSubjects,
|
||||||
'xmpTitleDescription': xmpTitleDescription,
|
'xmpTitleDescription': xmpTitleDescription,
|
||||||
|
@ -116,7 +120,7 @@ class CatalogMetadata {
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType#${shortHash(this)}{contentId=$contentId, mimeType=$mimeType, dateMillis=$dateMillis, isAnimated=$isAnimated, isFlipped=$isFlipped, isGeotiff=$isGeotiff, is360=$is360, rotationDegrees=$rotationDegrees, latitude=$latitude, longitude=$longitude, xmpSubjects=$xmpSubjects, xmpTitleDescription=$xmpTitleDescription}';
|
String toString() => '$runtimeType#${shortHash(this)}{contentId=$contentId, mimeType=$mimeType, dateMillis=$dateMillis, isAnimated=$isAnimated, isFlipped=$isFlipped, isGeotiff=$isGeotiff, is360=$is360, isMultipage=$isMultipage, rotationDegrees=$rotationDegrees, latitude=$latitude, longitude=$longitude, xmpSubjects=$xmpSubjects, xmpTitleDescription=$xmpTitleDescription}';
|
||||||
}
|
}
|
||||||
|
|
||||||
class OverlayMetadata {
|
class OverlayMetadata {
|
||||||
|
|
|
@ -64,6 +64,7 @@ class AIcons {
|
||||||
// thumbnail overlay
|
// thumbnail overlay
|
||||||
static const IconData animated = Icons.slideshow;
|
static const IconData animated = Icons.slideshow;
|
||||||
static const IconData geo = Icons.language_outlined;
|
static const IconData geo = Icons.language_outlined;
|
||||||
|
static const IconData multipage = Icons.burst_mode_outlined;
|
||||||
static const IconData play = Icons.play_circle_outline;
|
static const IconData play = Icons.play_circle_outline;
|
||||||
static const IconData threesixty = Icons.threesixty_outlined;
|
static const IconData threesixty = Icons.threesixty_outlined;
|
||||||
static const IconData selected = Icons.check_circle_outline;
|
static const IconData selected = Icons.check_circle_outline;
|
||||||
|
|
|
@ -36,6 +36,7 @@ class ThumbnailEntryOverlay extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
if (entry.hasGps && settings.showThumbnailLocation) GpsIcon(iconSize: iconSize),
|
if (entry.hasGps && settings.showThumbnailLocation) GpsIcon(iconSize: iconSize),
|
||||||
if (entry.isRaw && settings.showThumbnailRaw) RawIcon(iconSize: iconSize),
|
if (entry.isRaw && settings.showThumbnailRaw) RawIcon(iconSize: iconSize),
|
||||||
|
if (entry.isMultipage) MultipageIcon(iconSize: iconSize),
|
||||||
if (entry.isGeotiff) GeotiffIcon(iconSize: iconSize),
|
if (entry.isGeotiff) GeotiffIcon(iconSize: iconSize),
|
||||||
if (entry.isAnimated)
|
if (entry.isAnimated)
|
||||||
AnimatedImageIcon(iconSize: iconSize)
|
AnimatedImageIcon(iconSize: iconSize)
|
||||||
|
|
|
@ -102,6 +102,21 @@ class RawIcon extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MultipageIcon extends StatelessWidget {
|
||||||
|
final double iconSize;
|
||||||
|
|
||||||
|
const MultipageIcon({Key key, this.iconSize}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return OverlayIcon(
|
||||||
|
icon: AIcons.multipage,
|
||||||
|
size: iconSize,
|
||||||
|
iconScale: .8,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class OverlayIcon extends StatelessWidget {
|
class OverlayIcon extends StatelessWidget {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final double size;
|
final double size;
|
||||||
|
|
Loading…
Reference in a new issue