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]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
|
- Support for TIFF images (single page)
|
||||||
- Viewer overlay: minimap (optional)
|
- Viewer overlay: minimap (optional)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
@ -99,6 +99,7 @@ dependencies {
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.3.1'
|
implementation 'androidx.exifinterface:exifinterface:1.3.1'
|
||||||
implementation 'com.commonsware.cwac:document:0.4.1'
|
implementation 'com.commonsware.cwac:document:0.4.1'
|
||||||
implementation 'com.drewnoakes:metadata-extractor:2.15.0'
|
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'
|
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||||
|
|
||||||
kapt 'androidx.annotation:annotation:1.1.0'
|
kapt 'androidx.annotation:annotation:1.1.0'
|
||||||
|
|
|
@ -37,9 +37,13 @@ class RegionFetcher internal constructor(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (currentDecoderRef == null) {
|
if (currentDecoderRef == null) {
|
||||||
val newDecoder = StorageUtils.openInputStream(context, uri).use { input ->
|
val newDecoder = StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
BitmapRegionDecoder.newInstance(input, false)
|
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)
|
currentDecoderRef = LastDecoderRef(uri, newDecoder)
|
||||||
}
|
}
|
||||||
val decoder = currentDecoderRef.decoder
|
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.isVideo
|
||||||
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterContentResolverThumbnail
|
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterContentResolverThumbnail
|
||||||
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
|
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
|
||||||
|
import deckers.thibault.aves.utils.StorageUtils
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
class ThumbnailFetcher internal constructor(
|
class ThumbnailFetcher internal constructor(
|
||||||
private val activity: Activity,
|
private val activity: Activity,
|
||||||
|
@ -43,12 +47,13 @@ class ThumbnailFetcher internal constructor(
|
||||||
var recycle = true
|
var recycle = true
|
||||||
var exception: Exception? = null
|
var exception: Exception? = null
|
||||||
|
|
||||||
// fetch low quality thumbnails when size is not specified
|
if (mimeType == MimeTypes.TIFF) {
|
||||||
if ((width == defaultSize || height == defaultSize) && !isFlipped) {
|
bitmap = getTiff()
|
||||||
// as of Android R, the Media Store content resolver may return a thumbnail
|
} else if ((width == defaultSize || height == defaultSize) && !isFlipped) {
|
||||||
// that is automatically rotated according to EXIF orientation,
|
// Fetch low quality thumbnails when size is not specified.
|
||||||
// but not flipped when necessary
|
// As of Android R, the Media Store content resolver may return a thumbnail
|
||||||
// so we skip this step for flipped entries
|
// that is automatically rotated according to EXIF orientation, but not flipped,
|
||||||
|
// so we skip this step for flipped entries.
|
||||||
try {
|
try {
|
||||||
bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) getByResolver() else getByMediaStore()
|
bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) getByResolver() else getByMediaStore()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -136,4 +141,44 @@ class ThumbnailFetcher internal constructor(
|
||||||
Glide.with(activity).clear(target)
|
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.isSupportedByFlutter
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
||||||
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
|
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
|
||||||
import io.flutter.plugin.common.EventChannel.EventSink
|
import io.flutter.plugin.common.EventChannel.EventSink
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
|
||||||
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
|
@ -71,6 +73,8 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
|
||||||
|
|
||||||
if (isVideo(mimeType)) {
|
if (isVideo(mimeType)) {
|
||||||
streamVideoByGlide(uri)
|
streamVideoByGlide(uri)
|
||||||
|
} else if (mimeType == MimeTypes.TIFF) {
|
||||||
|
streamTiffImage(uri)
|
||||||
} else if (!isSupportedByFlutter(mimeType, rotationDegrees, isFlipped)) {
|
} else if (!isSupportedByFlutter(mimeType, rotationDegrees, isFlipped)) {
|
||||||
// decode exotic format on platform side, then encode it in portable format for Flutter
|
// decode exotic format on platform side, then encode it in portable format for Flutter
|
||||||
streamImageByGlide(uri, mimeType, rotationDegrees, isFlipped)
|
streamImageByGlide(uri, mimeType, rotationDegrees, isFlipped)
|
||||||
|
@ -83,7 +87,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
|
||||||
|
|
||||||
private fun streamImageAsIs(uri: Uri) {
|
private fun streamImageAsIs(uri: Uri) {
|
||||||
try {
|
try {
|
||||||
openInputStream(activity, uri).use { input -> input?.let { streamBytes(it) } }
|
StorageUtils.openInputStream(activity, uri)?.use { input -> streamBytes(input) }
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
error("streamImage-image-read-exception", "failed to get image from uri=$uri", e.message)
|
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) {
|
private fun streamBytes(inputStream: InputStream) {
|
||||||
val buffer = ByteArray(bufferSize)
|
val buffer = ByteArray(bufferSize)
|
||||||
var len: Int
|
var len: Int
|
||||||
|
|
|
@ -58,7 +58,7 @@ object MimeTypes {
|
||||||
else -> false
|
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) {
|
fun isSupportedByFlutter(mimeType: String, rotationDegrees: Int?, isFlipped: Boolean?) = when (mimeType) {
|
||||||
JPEG, GIF, WEBP, BMP, WBMP, ICO, SVG -> true
|
JPEG, GIF, WEBP, BMP, WBMP, ICO, SVG -> true
|
||||||
PNG -> rotationDegrees ?: 0 == 0 && !(isFlipped ?: false)
|
PNG -> rotationDegrees ?: 0 == 0 && !(isFlipped ?: false)
|
||||||
|
|
|
@ -15,10 +15,7 @@ import android.webkit.MimeTypeMap
|
||||||
import com.commonsware.cwac.document.DocumentFileCompat
|
import com.commonsware.cwac.document.DocumentFileCompat
|
||||||
import deckers.thibault.aves.utils.LogUtils.createTag
|
import deckers.thibault.aves.utils.LogUtils.createTag
|
||||||
import deckers.thibault.aves.utils.PermissionManager.getGrantedDirForPath
|
import deckers.thibault.aves.utils.PermissionManager.getGrantedDirForPath
|
||||||
import java.io.File
|
import java.io.*
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.regex.Pattern
|
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
|
// convenience methods
|
||||||
|
|
||||||
fun ensureTrailingSeparator(dirPath: String): String {
|
fun ensureTrailingSeparator(dirPath: String): String {
|
||||||
|
|
|
@ -41,5 +41,5 @@ class MimeTypes {
|
||||||
|
|
||||||
// groups
|
// 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> 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 int infoGroupMaxValueLength = 140;
|
||||||
|
|
||||||
static const List<Dependency> androidDependencies = [
|
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(
|
Dependency(
|
||||||
name: 'CWAC-Document',
|
name: 'CWAC-Document',
|
||||||
license: 'Apache 2.0',
|
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
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 1.2.5+31
|
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):
|
# 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 content URIs (by default, but trivial by fork)
|
||||||
# - does not support AVI/XVID, AC3
|
# - does not support AVI/XVID, AC3
|
||||||
|
|
Loading…
Reference in a new issue