added TIFF support (single page)

This commit is contained in:
Thibault Deckers 2020-11-12 19:37:02 +09:00
parent af04d40556
commit cd4041fdbc
10 changed files with 120 additions and 15 deletions

View file

@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
### Added
- Support for TIFF images (single page)
- Viewer overlay: minimap (optional)
### Changed

View file

@ -99,6 +99,7 @@ dependencies {
implementation 'androidx.exifinterface:exifinterface:1.3.1'
implementation 'com.commonsware.cwac:document:0.4.1'
implementation 'com.drewnoakes:metadata-extractor:2.15.0'
implementation 'com.github.beyka:androidtiffbitmapfactory:0.9.8.7'
implementation 'com.github.bumptech.glide:glide:4.11.0'
kapt 'androidx.annotation:annotation:1.1.0'

View file

@ -37,9 +37,13 @@ class RegionFetcher internal constructor(
try {
if (currentDecoderRef == null) {
val newDecoder = StorageUtils.openInputStream(context, uri).use { input ->
val newDecoder = StorageUtils.openInputStream(context, uri)?.use { input ->
BitmapRegionDecoder.newInstance(input, false)
}
if (newDecoder == null) {
result.error("getRegion-read-null", "failed to open file for uri=$uri regionRect=$regionRect", null)
return
}
currentDecoderRef = LastDecoderRef(uri, newDecoder)
}
val decoder = currentDecoderRef.decoder

View file

@ -20,7 +20,11 @@ import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterContentResolverThumbnail
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.MethodChannel
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
import java.io.File
import java.io.IOException
class ThumbnailFetcher internal constructor(
private val activity: Activity,
@ -43,12 +47,13 @@ class ThumbnailFetcher internal constructor(
var recycle = true
var exception: Exception? = null
// fetch low quality thumbnails when size is not specified
if ((width == defaultSize || height == defaultSize) && !isFlipped) {
// as of Android R, the Media Store content resolver may return a thumbnail
// that is automatically rotated according to EXIF orientation,
// but not flipped when necessary
// so we skip this step for flipped entries
if (mimeType == MimeTypes.TIFF) {
bitmap = getTiff()
} else if ((width == defaultSize || height == defaultSize) && !isFlipped) {
// Fetch low quality thumbnails when size is not specified.
// As of Android R, the Media Store content resolver may return a thumbnail
// that is automatically rotated according to EXIF orientation, but not flipped,
// so we skip this step for flipped entries.
try {
bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) getByResolver() else getByMediaStore()
} catch (e: Exception) {
@ -136,4 +141,44 @@ class ThumbnailFetcher internal constructor(
Glide.with(activity).clear(target)
}
}
private fun getTiff(): Bitmap? {
// copy source stream to a temp file
val file: File
try {
file = File.createTempFile("aves", ".tiff")
StorageUtils.openInputStream(activity, uri)?.use { input ->
StorageUtils.copyInputStreamToFile(input, file)
}
file.deleteOnExit()
} catch (e: IOException) {
return null
}
// check directory count
val options = TiffBitmapFactory.Options().apply {
inJustDecodeBounds = true
}
TiffBitmapFactory.decodeFile(file, options)
if (options.outDirectoryCount == 0) return null
options.inDirectoryNumber = 0
// determine sample size
TiffBitmapFactory.decodeFile(file, options)
val imageWidth = options.outWidth
val imageHeight = options.outHeight
var sampleSize = 1
if (imageHeight > height || imageWidth > width) {
while (imageHeight / (sampleSize * 2) > height && imageWidth / (sampleSize * 2) > width) {
sampleSize *= 2
}
}
// decode
with(options) {
inJustDecodeBounds = false
inSampleSize = sampleSize
}
return TiffBitmapFactory.decodeFile(file, options)
}
}

View file

@ -15,11 +15,13 @@ import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.isSupportedByFlutter
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
import deckers.thibault.aves.utils.StorageUtils.openInputStream
import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
import java.io.File
import java.io.IOException
import java.io.InputStream
@ -71,6 +73,8 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
if (isVideo(mimeType)) {
streamVideoByGlide(uri)
} else if (mimeType == MimeTypes.TIFF) {
streamTiffImage(uri)
} 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)
@ -83,7 +87,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
private fun streamImageAsIs(uri: Uri) {
try {
openInputStream(activity, uri).use { input -> input?.let { streamBytes(it) } }
StorageUtils.openInputStream(activity, uri)?.use { input -> streamBytes(input) }
} catch (e: IOException) {
error("streamImage-image-read-exception", "failed to get image from uri=$uri", e.message)
}
@ -136,6 +140,38 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
}
}
private fun streamTiffImage(uri: Uri) {
// copy source stream to a temp file
val file: File
try {
file = File.createTempFile("aves", ".tiff")
StorageUtils.openInputStream(activity, uri)?.use { input ->
StorageUtils.copyInputStreamToFile(input, file)
}
file.deleteOnExit()
} catch (e: IOException) {
error("streamImage-tiff-copy", "failed to copy file from uri=$uri", null)
return
}
val options = TiffBitmapFactory.Options()
options.inJustDecodeBounds = true
TiffBitmapFactory.decodeFile(file, options)
val dirCount: Int = options.outDirectoryCount
// TODO TLAD handle multipage TIFF
if (dirCount > 0) {
val i = 0
options.inDirectoryNumber = i
options.inJustDecodeBounds = false
val bitmap = TiffBitmapFactory.decodeFile(file, options)
if (bitmap != null) {
success(bitmap.getBytes(canHaveAlpha = true, recycle = true))
} else {
error("streamImage-tiff-null", "failed to get tiff image (dir=$i) from uri=$uri", null)
}
}
}
private fun streamBytes(inputStream: InputStream) {
val buffer = ByteArray(bufferSize)
var len: Int

View file

@ -58,7 +58,7 @@ object MimeTypes {
else -> false
}
// as of Flutter v1.22.0
// as of Flutter v1.22.0, with additional custom handling for SVG
fun isSupportedByFlutter(mimeType: String, rotationDegrees: Int?, isFlipped: Boolean?) = when (mimeType) {
JPEG, GIF, WEBP, BMP, WBMP, ICO, SVG -> true
PNG -> rotationDegrees ?: 0 == 0 && !(isFlipped ?: false)

View file

@ -15,10 +15,7 @@ import android.webkit.MimeTypeMap
import com.commonsware.cwac.document.DocumentFileCompat
import deckers.thibault.aves.utils.LogUtils.createTag
import deckers.thibault.aves.utils.PermissionManager.getGrantedDirForPath
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import java.io.*
import java.util.*
import java.util.regex.Pattern
@ -408,6 +405,17 @@ object StorageUtils {
}
}
@Throws(IOException::class)
fun copyInputStreamToFile(inputStream: InputStream, file: File) {
FileOutputStream(file).use { outputStream ->
var read: Int
val bytes = ByteArray(1024)
while (inputStream.read(bytes).also { read = it } != -1) {
outputStream.write(bytes, 0, read)
}
}
}
// convenience methods
fun ensureTrailingSeparator(dirPath: String): String {

View file

@ -41,5 +41,5 @@ class MimeTypes {
// groups
static const List<String> rawImages = [arw, cr2, crw, dcr, dng, erf, k25, kdc, mrw, nef, nrw, orf, pef, raf, raw, rw2, sr2, srf, srw, x3f];
static const List<String> undecodable = [crw, psd, tiff]; // TODO TLAD make it dynamic if it depends on OS/lib versions
static const List<String> undecodable = [crw, psd]; // TODO TLAD make it dynamic if it depends on OS/lib versions
}

View file

@ -26,6 +26,12 @@ class Constants {
static const int infoGroupMaxValueLength = 140;
static const List<Dependency> androidDependencies = [
Dependency(
name: 'Android-TiffBitmapFactory',
license: 'MIT',
licenseUrl: 'https://github.com/Beyka/Android-TiffBitmapFactory/blob/master/license.txt',
sourceUrl: 'https://github.com/Beyka/Android-TiffBitmapFactory',
),
Dependency(
name: 'CWAC-Document',
license: 'Apache 2.0',

View file

@ -17,6 +17,10 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.2.5+31
# brendan-duncan/image (as of v2.1.19):
# - does not support TIFF with JPEG compression (issue #184)
# - TIFF tile decoding is not public (issue #258)
# video_player (as of v0.10.8+2, backed by ExoPlayer):
# - does not support content URIs (by default, but trivial by fork)
# - does not support AVI/XVID, AC3