diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt index 2a5355ba8..689d79458 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt @@ -594,14 +594,16 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { val metadata = ImageMetadataReader.readMetadata(input) val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java) try { - fun getProp(propName: String): Int? = xmpDirs.map { it.xmpMeta.getPropertyInteger(XMP.GPANO_SCHEMA_NS, propName) }.firstOrNull { it != null } + fun getIntProp(propName: String): Int? = xmpDirs.map { it.xmpMeta.getPropertyInteger(XMP.GPANO_SCHEMA_NS, propName) }.firstOrNull { it != null } + fun getStringProp(propName: String): String? = xmpDirs.map { it.xmpMeta.getPropertyString(XMP.GPANO_SCHEMA_NS, propName) }.firstOrNull { it != null } val fields: FieldMap = hashMapOf( - "croppedAreaLeft" to getProp(XMP.GPANO_CROPPED_AREA_LEFT_PROP_NAME), - "croppedAreaTop" to getProp(XMP.GPANO_CROPPED_AREA_TOP_PROP_NAME), - "croppedAreaWidth" to getProp(XMP.GPANO_CROPPED_AREA_WIDTH_PROP_NAME), - "croppedAreaHeight" to getProp(XMP.GPANO_CROPPED_AREA_HEIGHT_PROP_NAME), - "fullPanoWidth" to getProp(XMP.GPANO_FULL_PANO_WIDTH_PROP_NAME), - "fullPanoHeight" to getProp(XMP.GPANO_FULL_PANO_HEIGHT_PROP_NAME), + "croppedAreaLeft" to getIntProp(XMP.GPANO_CROPPED_AREA_LEFT_PROP_NAME), + "croppedAreaTop" to getIntProp(XMP.GPANO_CROPPED_AREA_TOP_PROP_NAME), + "croppedAreaWidth" to getIntProp(XMP.GPANO_CROPPED_AREA_WIDTH_PROP_NAME), + "croppedAreaHeight" to getIntProp(XMP.GPANO_CROPPED_AREA_HEIGHT_PROP_NAME), + "fullPanoWidth" to getIntProp(XMP.GPANO_FULL_PANO_WIDTH_PROP_NAME), + "fullPanoHeight" to getIntProp(XMP.GPANO_FULL_PANO_HEIGHT_PROP_NAME), + "projectionType" to (getStringProp(XMP.GPANO_PROJECTION_TYPE_PROP_NAME) ?: XMP.GPANO_PROJECTION_TYPE_DEFAULT), ) result.success(fields) return diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt index bd7ff1733..0ca46696b 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt @@ -54,18 +54,19 @@ object XMP { const val GPANO_CROPPED_AREA_TOP_PROP_NAME = "GPano:CroppedAreaTopPixels" const val GPANO_FULL_PANO_HEIGHT_PROP_NAME = "GPano:FullPanoHeightPixels" const val GPANO_FULL_PANO_WIDTH_PROP_NAME = "GPano:FullPanoWidthPixels" - private const val GPANO_PROJECTION_TYPE_PROP_NAME = "GPano:ProjectionType" + const val GPANO_PROJECTION_TYPE_PROP_NAME = "GPano:ProjectionType" + const val GPANO_PROJECTION_TYPE_DEFAULT = "equirectangular" private const val PMTM_IS_PANO360 = "pmtm:IsPano360" + // `GPano:ProjectionType` is required by spec but it is sometimes missing, assuming default + // `GPano:FullPanoHeightPixels` is required by spec but it is sometimes missing (e.g. Samsung Camera app panorama mode) private val gpanoRequiredProps = listOf( GPANO_CROPPED_AREA_HEIGHT_PROP_NAME, GPANO_CROPPED_AREA_WIDTH_PROP_NAME, GPANO_CROPPED_AREA_LEFT_PROP_NAME, GPANO_CROPPED_AREA_TOP_PROP_NAME, - GPANO_FULL_PANO_HEIGHT_PROP_NAME, GPANO_FULL_PANO_WIDTH_PROP_NAME, - GPANO_PROJECTION_TYPE_PROP_NAME, ) // extensions diff --git a/lib/model/panorama.dart b/lib/model/panorama.dart index 0bebe9501..99d1ff318 100644 --- a/lib/model/panorama.dart +++ b/lib/model/panorama.dart @@ -4,24 +4,38 @@ import 'package:flutter/widgets.dart'; class PanoramaInfo { final Rect croppedAreaRect; final Size fullPanoSize; + final String projectionType; PanoramaInfo({ this.croppedAreaRect, this.fullPanoSize, + this.projectionType, }); factory PanoramaInfo.fromMap(Map map) { - final cLeft = map['croppedAreaLeft'] as int; - final cTop = map['croppedAreaTop'] as int; + var cLeft = map['croppedAreaLeft'] as int; + var cTop = map['croppedAreaTop'] as int; final cWidth = map['croppedAreaWidth'] as int; final cHeight = map['croppedAreaHeight'] as int; + var fWidth = map['fullPanoWidth'] as int; + var fHeight = map['fullPanoHeight'] as int; + final projectionType = map['projectionType'] as String; + + // handle missing `fullPanoHeight` (e.g. Samsung camera app panorama mode) + if (fHeight == null && cWidth != null && cHeight != null) { + // assume the cropped area is actually covering 360 degrees horizontally + // even when `croppedAreaLeft` is non zero + fWidth = cWidth; + fHeight = (fWidth / 2).round(); + cTop = ((fHeight - cHeight) / 2).round(); + cLeft = 0; + } + Rect croppedAreaRect; if (cLeft != null && cTop != null && cWidth != null && cHeight != null) { croppedAreaRect = Rect.fromLTWH(cLeft.toDouble(), cTop.toDouble(), cWidth.toDouble(), cHeight.toDouble()); } - final fWidth = map['fullPanoWidth'] as int; - final fHeight = map['fullPanoHeight'] as int; Size fullPanoSize; if (fWidth != null && fHeight != null) { fullPanoSize = Size(fWidth.toDouble(), fHeight.toDouble()); @@ -30,11 +44,12 @@ class PanoramaInfo { return PanoramaInfo( croppedAreaRect: croppedAreaRect, fullPanoSize: fullPanoSize, + projectionType: projectionType, ); } bool get hasCroppedArea => croppedAreaRect != null && fullPanoSize != null; @override - String toString() => '$runtimeType#${shortHash(this)}{croppedAreaRect=$croppedAreaRect, fullPanoSize=$fullPanoSize}'; + String toString() => '$runtimeType#${shortHash(this)}{croppedAreaRect=$croppedAreaRect, fullPanoSize=$fullPanoSize, projectionType=$projectionType}'; }