Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
deckerst committed May 1, 2024
2 parents ed75dba + e359eb4 commit 645c199
Show file tree
Hide file tree
Showing 134 changed files with 1,132 additions and 9,814 deletions.
2 changes: 1 addition & 1 deletion .flutter
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file.

## <a id="unreleased"></a>[Unreleased]

## <a id="v1.11.0"></a>[v1.11.0] - 2024-05-01

### Added

- Cataloguing: identify Apple variant of HDR images
- Collection: allow using hash (md5/sha1/sha256) when bulk renaming
- Info: color palette
- Video: external subtitle support (SRT)
- option to force using western arabic numerals for dates

### Changed

- logo
- upgraded Flutter to stable v3.19.6

### Fixed

- rendering of SVG with large header
- stopping video playback when changing device orientation on Android >=13
- printing content orientation according to page format

## <a id="v1.10.9"></a>[v1.10.9] - 2024-04-14

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion android/app/src/debug/res/values/colors.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_flavour">#7B1FA2</color>
<color name="ic_launcher_flavour">#815AFA</color>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,13 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
}

val pageIndex = id - 1
val mpEntries = MultiPage.getJpegMpfEntries(context, uri)
val mpEntries = MultiPage.getJpegMpfEntries(context, uri, sizeBytes)
if (mpEntries != null && pageIndex < mpEntries.size) {
val mpEntry = mpEntries[pageIndex]
mpEntry.mimeType?.let { embedMimeType ->
var dataOffset = mpEntry.dataOffset
if (dataOffset > 0) {
val baseOffset = MultiPage.getJpegMpfBaseOffset(context, uri)
val baseOffset = MultiPage.getJpegMpfBaseOffset(context, uri, sizeBytes)
if (baseOffset != null) {
dataOffset += baseOffset
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.drew.metadata.exif.ExifDirectoryBase
import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.exif.ExifSubIFDDirectory
import com.drew.metadata.exif.GpsDirectory
import com.drew.metadata.exif.makernotes.AppleMakernoteDirectory
import com.drew.metadata.file.FileTypeDirectory
import com.drew.metadata.gif.GifAnimationDirectory
import com.drew.metadata.iptc.IptcDirectory
Expand Down Expand Up @@ -69,6 +70,8 @@ import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeRational
import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeString
import deckers.thibault.aves.metadata.metadataextractor.Helper.isPngTextDir
import deckers.thibault.aves.metadata.metadataextractor.PngActlDirectory
import deckers.thibault.aves.metadata.metadataextractor.SafeXmpReader
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntry
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory
import deckers.thibault.aves.metadata.xmp.GoogleXMP
import deckers.thibault.aves.metadata.xmp.XMP
Expand All @@ -82,6 +85,7 @@ import deckers.thibault.aves.metadata.xmp.XMP.isMotionPhoto
import deckers.thibault.aves.metadata.xmp.XMP.isPanorama
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue
import deckers.thibault.aves.utils.HashUtils
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.TIFF_EXTENSION_PATTERN
Expand All @@ -101,6 +105,7 @@ import org.json.JSONObject
import java.nio.charset.StandardCharsets
import java.text.DecimalFormat
import java.text.ParseException
import java.util.Locale
import kotlin.math.roundToInt
import kotlin.math.roundToLong

Expand Down Expand Up @@ -392,6 +397,21 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// do not overwrite XMP parsed by metadata-extractor
// with raw XMP found by ExifInterface
allTags.remove(Metadata.DIR_XMP)
} else {
val xmpTags = allTags[Metadata.DIR_XMP]
if (xmpTags != null) {
val xmpRaw = xmpTags[ExifInterface.TAG_XMP]
if (xmpRaw != null) {
val metadata = com.drew.metadata.Metadata()
val xmpBytes = xmpRaw.toByteArray(Charsets.UTF_8)
SafeXmpReader().extract(xmpBytes, 0, xmpBytes.size, metadata, null)
metadata.getFirstDirectoryOfType(XmpDirectory::class.java)?.let { xmpDir ->
val dirMap = HashMap<String, String>()
processXmp(xmpDir.xmpMeta, dirMap, allowMultiple = true)
allTags[Metadata.DIR_XMP] = dirMap
}
}
}
}
metadataMap.putAll(allTags.mapValues { it.value.toMutableMap() })
}
Expand Down Expand Up @@ -639,6 +659,10 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// JPEG Multi-Picture Format
if (metadata.getDirectoriesOfType(MpEntryDirectory::class.java).count { !it.entry.isThumbnail } > 1) {
flags = flags or MASK_IS_MULTIPAGE

if (hasAppleHdrGainMap(uri, sizeBytes, metadata)) {
flags = flags or MASK_IS_HDR
}
}

// XMP
Expand Down Expand Up @@ -765,6 +789,29 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
metadataMap[KEY_FLAGS] = flags
}

private fun hasAppleHdrGainMap(uri: Uri, sizeBytes: Long?, primaryMetadata: com.drew.metadata.Metadata): Boolean {
if (!primaryMetadata.containsDirectoryOfType(AppleMakernoteDirectory::class.java)) return false

val mpEntries = MultiPage.getJpegMpfEntries(context, uri, sizeBytes) ?: return false
mpEntries.filter { it.type == MpEntry.TYPE_UNDEFINED }.forEach { mpEntry ->
var dataOffset = mpEntry.dataOffset
if (dataOffset > 0) {
val baseOffset = MultiPage.getJpegMpfBaseOffset(context, uri, sizeBytes)
if (baseOffset != null) {
dataOffset += baseOffset
}
}
StorageUtils.openInputStream(context, uri)?.let { input ->
input.skip(dataOffset)
val pageMetadata = Helper.safeRead(input)
if (pageMetadata.getDirectoriesOfType(XmpDirectory::class.java).any { it.xmpMeta.hasHdrGainMap() }) {
return true
}
}
}
return false
}

private fun getMultimediaCatalogMetadataByMediaMetadataRetriever(
mimeType: String,
uri: Uri,
Expand Down Expand Up @@ -1004,7 +1051,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
} else {
when (mimeType) {
MimeTypes.HEIC, MimeTypes.HEIF -> MultiPage.getHeicTracks(context, uri)
MimeTypes.JPEG -> MultiPage.getJpegMpfPages(context, uri)
MimeTypes.JPEG -> MultiPage.getJpegMpfPages(context, uri, sizeBytes)
MimeTypes.TIFF -> MultiPage.getTiffPages(context, uri)
else -> null
}
Expand Down Expand Up @@ -1262,10 +1309,36 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
return
}

val metadataMap = HashMap<String, Any>()
val metadataMap = HashMap<String, Any?>()

val hashFields = fields.filter { it.startsWith(HASH_FIELD_PREFIX) }.toSet()
metadataMap.putAll(getHashFields(uri, mimeType, sizeBytes, hashFields))

val exifFields = fields.filterNot { hashFields.contains(it) }.toSet()
metadataMap.putAll(getExifFields(uri, mimeType, sizeBytes, exifFields))

result.success(metadataMap)
}

private fun getHashFields(uri: Uri, mimeType: String, sizeBytes: Long?, fields: Set<String>): FieldMap {
val metadataMap = HashMap<String, Any?>()
fields.forEach { field ->
val function = field.substringAfter(HASH_FIELD_PREFIX).lowercase(Locale.ROOT)
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
metadataMap[field] = HashUtils.getHash(input, function)
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get hash for mimeType=$mimeType uri=$uri function=$function", e)
}
}
return metadataMap
}

private fun getExifFields(uri: Uri, mimeType: String, sizeBytes: Long?, fields: Set<String>): FieldMap {
val metadataMap = HashMap<String, Any?>()
if (fields.isEmpty() || isVideo(mimeType)) {
result.success(metadataMap)
return
return metadataMap
}

var foundExif = false
Expand Down Expand Up @@ -1314,7 +1387,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
}

result.success(metadataMap)
return metadataMap
}

companion object {
Expand Down Expand Up @@ -1389,6 +1462,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// additional media key
private const val KEY_HAS_EMBEDDED_PICTURE = "Has Embedded Picture"

private const val HASH_FIELD_PREFIX = "hash"
private const val VALUE_SKIPPED_DATA = "[skipped]"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
return
}

val trashDirs = context.getExternalFilesDirs(null).mapNotNull { StorageUtils.trashDirFor(context, it.path) }
val trashDirs = context.getExternalFilesDirs(null).filterNotNull().mapNotNull { StorageUtils.trashDirFor(context, it.path) }
val trashItemPaths = trashDirs.flatMap { dir -> dir.listFiles()?.filterNotNull()?.mapNotNull { file -> file.path } ?: listOf() }
val untrackedPaths = trashItemPaths.filterNot(knownPaths::contains).toList()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.caverock.androidsvg.PreserveAspectRatio
import com.caverock.androidsvg.RenderOptions
import com.caverock.androidsvg.SVG
import com.caverock.androidsvg.SVGParseException
import deckers.thibault.aves.metadata.SVGParserBufferedInputStream
import deckers.thibault.aves.metadata.SvgHelper.normalizeSize
import deckers.thibault.aves.utils.BitmapUtils.ARGB_8888_BYTE_SIZE
import deckers.thibault.aves.utils.BitmapUtils.getBytes
Expand Down Expand Up @@ -47,7 +48,7 @@ class SvgRegionFetcher internal constructor(
if (currentSvgRef == null) {
val newSvg = StorageUtils.openInputStream(context, uri)?.use { input ->
try {
SVG.getFromInputStream(input)
SVG.getFromInputStream(SVGParserBufferedInputStream(input))
} catch (ex: SVGParseException) {
result.error("fetch-parse", "failed to parse SVG for uri=$uri regionRect=$regionRect", null)
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ class ThumbnailFetcher internal constructor(

return try {
var bitmap = target.get()
if (needRotationAfterGlide(mimeType)) {
if (needRotationAfterGlide(mimeType, pageId)) {
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
}
bitmap
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,6 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
companion object {
private val LOG_TAG = LogUtils.createTag<ActivityResultStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/activity_result_stream"
private const val BUFFER_SIZE = 2 shl 17 // 256kB
private const val BUFFER_SIZE = 1 shl 18 // 256kB
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
.submit()
try {
var bitmap = withContext(Dispatchers.IO) { target.get() }
if (needRotationAfterGlide(mimeType)) {
if (needRotationAfterGlide(mimeType, pageId)) {
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
}
if (bitmap != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.bumptech.glide.module.LibraryGlideModule
import com.bumptech.glide.signature.ObjectKey
import com.caverock.androidsvg.SVG
import com.caverock.androidsvg.SVGParseException
import deckers.thibault.aves.metadata.SVGParserBufferedInputStream
import deckers.thibault.aves.metadata.SvgHelper.normalizeSize
import deckers.thibault.aves.utils.StorageUtils
import kotlin.math.ceil
Expand Down Expand Up @@ -52,15 +53,15 @@ internal class SvgFetcher(val model: SvgImage, val width: Int, val height: Int)

val bitmap: Bitmap? = StorageUtils.openInputStream(context, uri)?.use { input ->
try {
SVG.getFromInputStream(input)?.let { svg ->
SVG.getFromInputStream(SVGParserBufferedInputStream(input))?.let { svg ->
svg.normalizeSize()
val viewBox = svg.documentViewBox
val svgWidth = viewBox.width()
val svgHeight = viewBox.height()

val bitmapWidth: Int
val bitmapHeight: Int
if (width / height > svgWidth / svgHeight) {
if (width / height.toFloat() > svgWidth / svgHeight) {
bitmapWidth = ceil(svgWidth * height / svgHeight).toInt()
bitmapHeight = height
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ internal class TiffFetcher(val model: TiffImage, val width: Int, val height: Int
TiffBitmapFactory.decodeFileDescriptor(fd, options)
val imageWidth = options.outWidth
val imageHeight = options.outHeight
if (imageHeight > height || imageWidth > width) {
if (imageWidth > width || imageHeight > height) {
while (imageHeight / (sampleSize * 2) >= height && imageWidth / (sampleSize * 2) >= width) {
sampleSize *= 2
}
Expand Down
Loading

0 comments on commit 645c199

Please sign in to comment.