Skip to content

Commit

Permalink
#11 viewer: multipage TIFF support
Browse files Browse the repository at this point in the history
  • Loading branch information
deckerst committed Jan 11, 2021
1 parent 9ca5f7b commit a121d21
Show file tree
Hide file tree
Showing 51 changed files with 921 additions and 263 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -251,18 +251,18 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
TiffBitmapFactory.decodeFileDescriptor(fd, options)
metadataMap["0"] = tiffOptionsToMap(options)
val dirCount = options.outDirectoryCount
for (i in 1 until dirCount) {
for (page in 1 until dirCount) {
fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd()
if (fd == null) {
result.error("getTiffStructure-fd", "failed to get file descriptor", null)
return
}
options = TiffBitmapFactory.Options().apply {
inJustDecodeBounds = true
inDirectoryNumber = i
inDirectoryNumber = page
}
TiffBitmapFactory.decodeFileDescriptor(fd, options)
metadataMap["$i"] = tiffOptionsToMap(options)
metadataMap["$page"] = tiffOptionsToMap(options)
}
result.success(metadataMap)
} catch (e: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
val isFlipped = call.argument<Boolean>("isFlipped")
val widthDip = call.argument<Double>("widthDip")
val heightDip = call.argument<Double>("heightDip")
val page = call.argument<Int>("page")
val defaultSizeDip = call.argument<Double>("defaultSizeDip")

if (uri == null || mimeType == null || dateModifiedSecs == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null) {
Expand All @@ -75,6 +76,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
isFlipped,
width = (widthDip * density).roundToInt(),
height = (heightDip * density).roundToInt(),
page = page,
defaultSize = (defaultSizeDip * density).roundToInt(),
result,
).fetch()
Expand All @@ -83,6 +85,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
private fun getRegion(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val mimeType = call.argument<String>("mimeType")
val page = call.argument<Int>("page")
val sampleSize = call.argument<Int>("sampleSize")
val x = call.argument<Int>("regionX")
val y = call.argument<Int>("regionY")
Expand All @@ -102,7 +105,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
uri,
sampleSize,
regionRect,
page = 0,
page = page ?: 0,
result,
)
else -> regionFetcher.fetch(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ import com.adobe.internal.xmp.properties.XMPPropertyInfo
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
import com.drew.imaging.ImageMetadataReader
import com.drew.lang.Rational
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.*
import com.drew.metadata.file.FileTypeDirectory
import com.drew.metadata.gif.GifAnimationDirectory
import com.drew.metadata.iptc.IptcDirectory
Expand Down Expand Up @@ -72,6 +69,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
"getAllMetadata" -> GlobalScope.launch(Dispatchers.IO) { getAllMetadata(call, Coresult(result)) }
"getCatalogMetadata" -> GlobalScope.launch(Dispatchers.IO) { getCatalogMetadata(call, Coresult(result)) }
"getOverlayMetadata" -> GlobalScope.launch(Dispatchers.IO) { getOverlayMetadata(call, Coresult(result)) }
"getMultiPageInfo" -> GlobalScope.launch(Dispatchers.IO) { getMultiPageInfo(call, Coresult(result)) }
"getEmbeddedPictures" -> GlobalScope.launch(Dispatchers.IO) { getEmbeddedPictures(call, Coresult(result)) }
"getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { getExifThumbnails(call, Coresult(result)) }
"extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { extractXmpDataProp(call, Coresult(result)) }
Expand Down Expand Up @@ -109,7 +107,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
metadataMap[dirName] = dirMap

// tags
if (mimeType == MimeTypes.TIFF && dir is ExifIFD0Directory) {
if (mimeType == MimeTypes.TIFF && (dir is ExifIFD0Directory || dir is ExifThumbnailDirectory)) {
dirMap.putAll(dir.tags.map {
val name = if (it.hasTagName()) {
it.tagName
Expand Down Expand Up @@ -397,7 +395,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
}
}

if (mimeType == MimeTypes.TIFF && getTiffDirCount(uri) > 1) flags = flags or MASK_IS_MULTIPAGE
if (mimeType == MimeTypes.TIFF && isMultiPageTiff(uri)) flags = flags or MASK_IS_MULTIPAGE

metadataMap[KEY_FLAGS] = flags
}
Expand Down Expand Up @@ -514,6 +512,33 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
result.success(metadataMap)
}

private fun getMultiPageInfo(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (mimeType == null || uri == null) {
result.error("getMultiPageInfo-args", "failed because of missing arguments", null)
return
}

val pages = HashMap<Int, Any>()
if (mimeType == MimeTypes.TIFF) {
fun toMap(options: TiffBitmapFactory.Options): Map<String, Any> {
return hashMapOf(
"width" to options.outWidth,
"height" to options.outHeight,
)
}
getTiffPageInfo(uri, 0)?.let { first ->
pages[0] = toMap(first)
val pageCount = first.outDirectoryCount
for (i in 1 until pageCount) {
getTiffPageInfo(uri, i)?.let { pages[i] = toMap(it) }
}
}
}
result.success(pages)
}

private fun getEmbeddedPictures(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (uri == null) {
Expand Down Expand Up @@ -642,23 +667,25 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
result.error("extractXmpDataProp-empty", "failed to extract file from XMP uri=$uri prop=$dataPropPath", null)
}

private fun getTiffDirCount(uri: Uri): Int {
var dirCount = 1
private fun isMultiPageTiff(uri: Uri) = getTiffPageInfo(uri, 0)?.outDirectoryCount ?: 1 > 1

private fun getTiffPageInfo(uri: Uri, page: Int): TiffBitmapFactory.Options? {
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
return null
}
val options = TiffBitmapFactory.Options().apply {
inJustDecodeBounds = true
inDirectoryNumber = page
}
TiffBitmapFactory.decodeFileDescriptor(fd, options)
return options
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get TIFF dir count for uri=$uri", e)
Log.w(LOG_TAG, "failed to get TIFF page info for uri=$uri page=$page", e)
}
return dirCount
return null
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ class ThumbnailFetcher internal constructor(
private val isFlipped: Boolean,
width: Int?,
height: Int?,
page: Int?,
private val defaultSize: Int,
private val result: MethodChannel.Result,
) {
val uri: Uri = Uri.parse(uri)
val width: Int = if (width?.takeIf { it > 0 } != null) width else defaultSize
val height: Int = if (height?.takeIf { it > 0 } != null) height else defaultSize
private val uri: Uri = Uri.parse(uri)
private val width: Int = if (width?.takeIf { it > 0 } != null) width else defaultSize
private val height: Int = if (height?.takeIf { it > 0 } != null) height else defaultSize
private val page = page ?: 0

fun fetch() {
var bitmap: Bitmap? = null
Expand Down Expand Up @@ -108,7 +110,7 @@ class ThumbnailFetcher internal constructor(
// add signature to ignore cache for images which got modified but kept the same URI
var options = RequestOptions()
.format(DecodeFormat.PREFER_RGB_565)
.signature(ObjectKey("$dateModifiedSecs-$rotationDegrees-$isFlipped-$width"))
.signature(ObjectKey("$dateModifiedSecs-$rotationDegrees-$isFlipped-$width-$page"))
.override(width, height)

val target = if (isVideo(mimeType)) {
Expand All @@ -119,7 +121,7 @@ class ThumbnailFetcher internal constructor(
.load(VideoThumbnail(context, uri))
.submit(width, height)
} else {
val model: Any = if (mimeType == MimeTypes.TIFF) TiffThumbnail(context, uri) else uri
val model: Any = if (mimeType == MimeTypes.TIFF) TiffThumbnail(context, uri, page) else uri
Glide.with(context)
.asBitmap()
.apply(options)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import android.app.Activity
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
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.VideoThumbnail
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.isSupportedByFlutter
import deckers.thibault.aves.utils.MimeTypes.isVideo
Expand Down Expand Up @@ -39,15 +41,33 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
override fun onCancel(o: Any) {}

private fun success(bytes: ByteArray) {
handler.post { eventSink.success(bytes) }
handler.post {
try {
eventSink.success(bytes)
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to use event sink", e)
}
}
}

private fun error(errorCode: String, errorMessage: String, errorDetails: Any?) {
handler.post { eventSink.error(errorCode, errorMessage, errorDetails) }
handler.post {
try {
eventSink.error(errorCode, errorMessage, errorDetails)
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to use event sink", e)
}
}
}

private fun endOfStream() {
handler.post { eventSink.endOfStream() }
handler.post {
try {
eventSink.endOfStream()
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to use event sink", e)
}
}
}

// Supported image formats:
Expand All @@ -64,6 +84,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
val uri = (arguments["uri"] as String?)?.let { Uri.parse(it) }
val rotationDegrees = arguments["rotationDegrees"] as Int
val isFlipped = arguments["isFlipped"] as Boolean
val page = arguments["page"] as Int

if (mimeType == null || uri == null) {
error("streamImage-args", "failed because of missing arguments", null)
Expand All @@ -74,7 +95,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
if (isVideo(mimeType)) {
streamVideoByGlide(uri)
} else if (mimeType == MimeTypes.TIFF) {
streamTiffImage(uri)
streamTiffImage(uri, page)
} else if (!isSupportedByFlutter(mimeType, rotationDegrees, isFlipped)) {
// decode exotic format on platform side, then encode it in portable format for Flutter
streamImageByGlide(uri, mimeType, rotationDegrees, isFlipped)
Expand Down Expand Up @@ -139,34 +160,19 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
private fun streamTiffImage(uri: Uri, page: Int = 0) {
val resolver = activity.contentResolver
try {
var fd = resolver.openFileDescriptor(uri, "r")?.detachFd()
val fd = resolver.openFileDescriptor(uri, "r")?.detachFd()
if (fd == null) {
error("streamImage-tiff-fd", "failed to get file descriptor for uri=$uri", null)
return
}
var options = TiffBitmapFactory.Options().apply {
inJustDecodeBounds = true
val options = TiffBitmapFactory.Options().apply {
inDirectoryNumber = page
}
TiffBitmapFactory.decodeFileDescriptor(fd, options)
val dirCount = options.outDirectoryCount

// TODO TLAD handle multipage TIFF
if (dirCount > page) {
fd = resolver.openFileDescriptor(uri, "r")?.detachFd()
if (fd == null) {
error("streamImage-tiff-fd", "failed to get file descriptor for uri=$uri", null)
return
}
options = TiffBitmapFactory.Options().apply {
inJustDecodeBounds = false
inDirectoryNumber = page
}
val bitmap = TiffBitmapFactory.decodeFileDescriptor(fd, options)
if (bitmap != null) {
success(bitmap.getBytes(canHaveAlpha = true, recycle = true))
} else {
error("streamImage-tiff-null", "failed to get tiff image (dir=$page) from uri=$uri", null)
}
val bitmap = TiffBitmapFactory.decodeFileDescriptor(fd, options)
if (bitmap != null) {
success(bitmap.getBytes(canHaveAlpha = true, recycle = true))
} else {
error("streamImage-tiff-null", "failed to get tiff image (dir=$page) from uri=$uri", null)
}
} catch (e: Exception) {
error("streamImage-tiff-exception", "failed to get image from uri=$uri", toErrorDetails(e))
Expand All @@ -192,6 +198,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
}

companion object {
private val LOG_TAG = LogUtils.createTag(ImageByteStreamHandler::class.java)
const val CHANNEL = "deckers.thibault/aves/imagebytestream"

const val bufferSize = 2 shl 17 // 256kB
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,33 @@ class ImageOpStreamHandler(private val context: Context, private val arguments:

// {String uri, bool success, [Map<String, Object> newFields]}
private fun success(result: Map<String, *>) {
handler.post { eventSink.success(result) }
handler.post {
try {
eventSink.success(result)
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to use event sink", e)
}
}
}

private fun error(errorCode: String, errorMessage: String, errorDetails: Any?) {
handler.post { eventSink.error(errorCode, errorMessage, errorDetails) }
handler.post {
try {
eventSink.error(errorCode, errorMessage, errorDetails)
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to use event sink", e)
}
}
}

private fun endOfStream() {
handler.post { eventSink.endOfStream() }
handler.post {
try {
eventSink.endOfStream()
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to use event sink", e)
}
}
}

private suspend fun move() {
Expand Down
Loading

0 comments on commit a121d21

Please sign in to comment.