added TIFF support (single page)
This commit is contained in:
parent
af04d40556
commit
cd4041fdbc
10 changed files with 120 additions and 15 deletions
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue