collection: identify multipage TIFF, multitrack HEIC/HEIF

This commit is contained in:
Thibault Deckers 2021-01-08 11:28:14 +09:00
parent 075bb2f07c
commit cd2811be02
7 changed files with 79 additions and 16 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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