From b803aa69ff873c004ed658f26d9ce243b51ad61d Mon Sep 17 00:00:00 2001 From: IacobIonut01 Date: Mon, 11 Nov 2024 20:41:09 +0200 Subject: [PATCH] Update sketch, zoomimage and add support for vault image subsampling Signed-off-by: IacobIonut01 --- app/build.gradle.kts | 6 +- .../main/kotlin/com/dot/gallery/GalleryApp.kt | 4 - .../dot/gallery/core/decoder/DecoderExt.kt | 54 ++++-- .../decoder/EncryptedBitmapFactoryDecoder.kt | 134 +++----------- .../core/decoder/EncryptedDecoderExt.kt | 167 ++++++++++++++++++ .../core/decoder/EncryptedRegionDecoder.kt | 139 +++++++++++++++ .../core/decoder/EncryptedRegionDecoderExt.kt | 108 +++++++++++ .../decoder/EncryptedVideoFrameDecoder.kt | 66 +++---- .../gallery/core/decoder/SketchHeifDecoder.kt | 16 +- .../gallery/core/decoder/SketchJxlDecoder.kt | 13 +- .../gallery/core/decoder/ThumbnailDecoder.kt | 108 ----------- .../domain/model/DecryptedMedia.kt | 7 + .../presentation/edit/EditViewModel.kt | 4 +- .../edit/components/markup/MarkupPainter.kt | 2 - .../presentation/mediaview/MediaViewScreen.kt | 4 +- .../components/media/ZoomablePagerImage.kt | 22 ++- gradle/libs.versions.toml | 14 +- 17 files changed, 570 insertions(+), 298 deletions(-) create mode 100644 app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedDecoderExt.kt create mode 100644 app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedRegionDecoder.kt create mode 100644 app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedRegionDecoderExt.kt delete mode 100644 app/src/main/kotlin/com/dot/gallery/core/decoder/ThumbnailDecoder.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5eb799e7fe..be05cdec32 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,7 +22,7 @@ android { applicationId = "com.dot.gallery" minSdk = 30 targetSdk = 35 - versionCode = 31006 + versionCode = 31007 versionName = "3.1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -200,7 +200,9 @@ dependencies { // Sketch implementation(libs.sketch.compose) implementation(libs.sketch.view) - implementation(libs.sketch.animated) + implementation(libs.sketch.animated.gif) + implementation(libs.sketch.animated.heif) + implementation(libs.sketch.animated.webp) implementation(libs.sketch.extensions.compose) implementation(libs.sketch.http.ktor) implementation(libs.sketch.svg) diff --git a/app/src/main/kotlin/com/dot/gallery/GalleryApp.kt b/app/src/main/kotlin/com/dot/gallery/GalleryApp.kt index 65a9dbd3ac..5017e15303 100644 --- a/app/src/main/kotlin/com/dot/gallery/GalleryApp.kt +++ b/app/src/main/kotlin/com/dot/gallery/GalleryApp.kt @@ -15,12 +15,10 @@ import com.github.panpf.sketch.PlatformContext import com.github.panpf.sketch.SingletonSketch import com.github.panpf.sketch.Sketch import com.github.panpf.sketch.cache.DiskCache -import com.github.panpf.sketch.decode.supportAnimatedGif import com.github.panpf.sketch.decode.supportAnimatedHeif import com.github.panpf.sketch.decode.supportAnimatedWebp import com.github.panpf.sketch.decode.supportSvg import com.github.panpf.sketch.decode.supportVideoFrame -import com.github.panpf.sketch.http.KtorStack import com.github.panpf.sketch.request.supportPauseLoadWhenScrolling import com.github.panpf.sketch.request.supportSaveCellularTraffic import com.github.panpf.sketch.util.appCacheDirectory @@ -32,13 +30,11 @@ import javax.inject.Inject class GalleryApp : Application(), SingletonSketch.Factory, Configuration.Provider { override fun createSketch(context: PlatformContext): Sketch = Sketch.Builder(this).apply { - httpStack(KtorStack()) components { supportSaveCellularTraffic() supportPauseLoadWhenScrolling() supportSvg() supportVideoFrame() - supportAnimatedGif() supportAnimatedWebp() supportAnimatedHeif() supportHeifDecoder() diff --git a/app/src/main/kotlin/com/dot/gallery/core/decoder/DecoderExt.kt b/app/src/main/kotlin/com/dot/gallery/core/decoder/DecoderExt.kt index fde7b2999d..3f7ec9b827 100644 --- a/app/src/main/kotlin/com/dot/gallery/core/decoder/DecoderExt.kt +++ b/app/src/main/kotlin/com/dot/gallery/core/decoder/DecoderExt.kt @@ -1,10 +1,9 @@ package com.dot.gallery.core.decoder import android.graphics.Bitmap -import com.github.panpf.sketch.asSketchImage +import com.github.panpf.sketch.asImage import com.github.panpf.sketch.decode.DecodeResult import com.github.panpf.sketch.decode.ImageInfo -import com.github.panpf.sketch.decode.internal.appliedResize import com.github.panpf.sketch.decode.internal.createScaledTransformed import com.github.panpf.sketch.request.RequestContext import com.github.panpf.sketch.source.DataSource @@ -13,6 +12,34 @@ import com.github.panpf.sketch.util.computeScaleMultiplierWithOneSide import okio.buffer import kotlin.math.roundToInt +inline fun DataSource.getImageInfo( + requestContext: RequestContext, + mimeType: String, + getSize: (ByteArray) -> android.util.Size? +): ImageInfo { + openSource().use { src -> + val sourceData = src.buffer().readByteArray() + val originalSizeDecoded = getSize(sourceData) ?: android.util.Size(0, 0) + val size = if (requestContext.size == Size.Origin) { + Size(originalSizeDecoded.width, originalSizeDecoded.height) + } else { + val scale = computeScaleMultiplierWithOneSide( + sourceSize = Size(originalSizeDecoded.width, originalSizeDecoded.height), + targetSize = requestContext.size, + ) + Size( + width = (originalSizeDecoded.width * scale).roundToInt(), + height = (originalSizeDecoded.height * scale).roundToInt() + ) + } + return ImageInfo( + width = size.width, + height = size.height, + mimeType = mimeType, + ) + } +} + inline fun DataSource.withCustomDecoder( requestContext: RequestContext, mimeType: String, @@ -21,11 +48,10 @@ inline fun DataSource.withCustomDecoder( ): DecodeResult = openSource().use { src -> val sourceData = src.buffer().readByteArray() - val imageInfo: ImageInfo var transformeds: List? = null val originalSizeDecoded = getSize(sourceData) ?: android.util.Size(0, 0) val originalSize = Size(originalSizeDecoded.width, originalSizeDecoded.height) - val targetSize = requestContext.size!! + val targetSize = requestContext.size val scale = computeScaleMultiplierWithOneSide( sourceSize = originalSize, targetSize = targetSize, @@ -34,12 +60,13 @@ inline fun DataSource.withCustomDecoder( transformeds = listOf(createScaledTransformed(scale)) } + val imageInfo = getImageInfo( + requestContext = requestContext, + mimeType = mimeType, + getSize = getSize + ) + val decodedImage = if (requestContext.size == Size.Origin) { - imageInfo = ImageInfo( - width = originalSize.width, - height = originalSize.height, - mimeType = mimeType, - ) decodeSampled( sourceData, originalSize.width, @@ -50,11 +77,6 @@ inline fun DataSource.withCustomDecoder( width = (originalSize.width * scale).roundToInt(), height = (originalSize.height * scale).roundToInt() ) - imageInfo = ImageInfo( - width = dstSize.width, - height = dstSize.height, - mimeType = mimeType, - ) decodeSampled( sourceData, dstSize.width, @@ -64,11 +86,11 @@ inline fun DataSource.withCustomDecoder( val resize = requestContext.computeResize(imageInfo.size) DecodeResult( - image = decodedImage.asSketchImage(), + image = decodedImage.asImage(), imageInfo = imageInfo, dataFrom = dataFrom, resize = resize, transformeds = transformeds, extras = null - ).appliedResize(requestContext) + ) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedBitmapFactoryDecoder.kt b/app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedBitmapFactoryDecoder.kt index 89f1f08b22..fc6dd781da 100644 --- a/app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedBitmapFactoryDecoder.kt +++ b/app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedBitmapFactoryDecoder.kt @@ -1,25 +1,16 @@ package com.dot.gallery.core.decoder -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.BitmapRegionDecoder -import android.os.Build.VERSION -import android.os.Build.VERSION_CODES -import androidx.exifinterface.media.ExifInterface import com.dot.gallery.BuildConfig import com.dot.gallery.feature_node.data.data_source.KeychainHolder -import com.dot.gallery.feature_node.domain.model.EncryptedMedia import com.github.panpf.sketch.ComponentRegistry import com.github.panpf.sketch.Image -import com.github.panpf.sketch.asSketchImage +import com.github.panpf.sketch.asImage +import com.github.panpf.sketch.decode.DecodeConfig import com.github.panpf.sketch.decode.Decoder import com.github.panpf.sketch.decode.ImageInfo -import com.github.panpf.sketch.decode.ImageInvalidException import com.github.panpf.sketch.decode.internal.DecodeHelper import com.github.panpf.sketch.decode.internal.ExifOrientationHelper import com.github.panpf.sketch.decode.internal.HelperDecoder -import com.github.panpf.sketch.decode.internal.ImageFormat -import com.github.panpf.sketch.decode.internal.newDecodeConfigByQualityParams import com.github.panpf.sketch.decode.internal.supportBitmapRegionDecoder import com.github.panpf.sketch.fetch.FetchResult import com.github.panpf.sketch.request.ImageRequest @@ -28,9 +19,6 @@ import com.github.panpf.sketch.request.get import com.github.panpf.sketch.source.DataSource import com.github.panpf.sketch.source.FileDataSource import com.github.panpf.sketch.util.Rect -import com.github.panpf.sketch.util.Size -import com.github.panpf.sketch.util.toAndroidRect -import java.io.IOException fun ComponentRegistry.Builder.supportVaultDecoder(): ComponentRegistry.Builder = apply { addDecoder(EncryptedBitmapFactoryDecoder.Factory()) @@ -74,118 +62,46 @@ open class EncryptedBitmapFactoryDecoder( } } -/** - * Decode encrypted bitmap using BitmapFactory - */ -@Throws(IOException::class) -private fun DataSource.decodeEncryptedBitmap(keychainHolder: KeychainHolder, options: BitmapFactory.Options? = null): Bitmap? { - return with(this as FileDataSource) { - val encryptedFile = path.toFile() - val encryptedMedia = with(keychainHolder) { - encryptedFile.decrypt() - } - BitmapFactory.decodeByteArray( - encryptedMedia.bytes, - 0, - encryptedMedia.bytes.size, - options.apply { this?.outMimeType = encryptedMedia.mimeType } - ) - } -} - -/** - * Use EncryptedBitmapRegionDecoder to decode part of a bitmap region - */ -@Throws(IOException::class) -private fun DataSource.decodeEncryptedRegionBitmap( - keychainHolder: KeychainHolder, - srcRect: android.graphics.Rect, - options: BitmapFactory.Options? = null -): Bitmap? { - return with(this as FileDataSource) { - val encryptedFile = path.toFile() - val encryptedMedia = with(keychainHolder) { - encryptedFile.decrypt() - } - val regionDecoder = if (VERSION.SDK_INT >= VERSION_CODES.S) { - BitmapRegionDecoder.newInstance(encryptedMedia.bytes, 0, encryptedMedia.bytes.size) - } else { - @Suppress("DEPRECATION") - BitmapRegionDecoder.newInstance(encryptedMedia.bytes, 0, encryptedMedia.bytes.size, false) - } - try { - regionDecoder.decodeRegion(srcRect, options.apply { this?.outMimeType = encryptedMedia.mimeType }) - } finally { - regionDecoder.recycle() - } - } -} - -@Throws(IOException::class) -fun DataSource.readEncryptedExifOrientation(keychainHolder: KeychainHolder): Int { - return with(this as FileDataSource) { - val encryptedFile = path.toFile() - val encryptedMedia = with(keychainHolder) { - encryptedFile.decrypt() - } - encryptedMedia.bytes.inputStream().use { - ExifInterface(it).getAttributeInt( - ExifInterface.TAG_ORIENTATION, - ExifInterface.ORIENTATION_UNDEFINED - ) - } - } -} - private class EncryptedBitmapFactoryDecodeHelper(val request: ImageRequest, private val dataSource: DataSource) : DecodeHelper { private val keychainHolder = KeychainHolder(request.context) - override val imageInfo: ImageInfo by lazy { decodeImageInfo() } + override val imageInfo: ImageInfo by lazy { + dataSource.readEncryptedImageInfo(keychainHolder, exifOrientationHelper) + } override val supportRegion: Boolean by lazy { - ImageFormat.parseMimeType(imageInfo.mimeType)?.supportBitmapRegionDecoder() == true + // The result returns null, which means unknown, but future versions may support it, so it is still worth trying. + supportBitmapRegionDecoder(imageInfo.mimeType) ?: true } private val exifOrientation: Int by lazy { dataSource.readEncryptedExifOrientation(keychainHolder) } private val exifOrientationHelper by lazy { ExifOrientationHelper(exifOrientation) } override fun decode(sampleSize: Int): Image { - val config = request.newDecodeConfigByQualityParams(imageInfo.mimeType).apply { - inSampleSize = sampleSize + val decodeConfig = DecodeConfig(request, imageInfo.mimeType, isOpaque = false).apply { + this.sampleSize = sampleSize } - val options = config.toBitmapOptions() - val bitmap = dataSource.decodeEncryptedBitmap(keychainHolder, options) - ?: throw ImageInvalidException("Invalid image. decode return null") - val image = bitmap.asSketchImage() - val correctedImage = exifOrientationHelper.applyToImage(image) ?: image - return correctedImage + val bitmap = dataSource.decodeEncryptedBitmap( + keychainHolder = keychainHolder, + config = decodeConfig, + exifOrientationHelper = exifOrientationHelper + ) + return bitmap.asImage() } override fun decodeRegion(region: Rect, sampleSize: Int): Image { - val config = request.newDecodeConfigByQualityParams(imageInfo.mimeType).apply { - inSampleSize = sampleSize + val decodeConfig = DecodeConfig(request, imageInfo.mimeType, isOpaque = false).apply { + this.sampleSize = sampleSize } - val options = config.toBitmapOptions() - val originalRegion = - exifOrientationHelper.applyToRect(region, imageInfo.size, reverse = true) - val bitmap = dataSource.decodeEncryptedRegionBitmap(keychainHolder, originalRegion.toAndroidRect(), options) - ?: throw ImageInvalidException("Invalid image. region decode return null") - val image = bitmap.asSketchImage() - val correctedImage = exifOrientationHelper.applyToImage(image) ?: image - return correctedImage - } - - private fun decodeImageInfo(): ImageInfo { - val boundOptions = BitmapFactory.Options().apply { - inJustDecodeBounds = true - } - dataSource.decodeEncryptedBitmap(keychainHolder, boundOptions) - val mimeType = boundOptions.outMimeType - val imageSize = Size(width = boundOptions.outWidth, height = boundOptions.outWidth) - val correctedImageSize = exifOrientationHelper.applyToSize(imageSize) - - return ImageInfo(size = correctedImageSize, mimeType = mimeType) + val bitmap = dataSource.decodeEncryptedRegionBitmap( + keychainHolder = keychainHolder, + srcRect = region, + config = decodeConfig, + imageSize = imageInfo.size, + exifOrientationHelper = exifOrientationHelper + ) + return bitmap.asImage() } override fun close() { diff --git a/app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedDecoderExt.kt b/app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedDecoderExt.kt new file mode 100644 index 0000000000..93f288b586 --- /dev/null +++ b/app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedDecoderExt.kt @@ -0,0 +1,167 @@ +package com.dot.gallery.core.decoder + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.BitmapRegionDecoder +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES +import androidx.exifinterface.media.ExifInterface +import com.dot.gallery.feature_node.data.data_source.KeychainHolder +import com.dot.gallery.feature_node.domain.model.EncryptedMedia +import com.github.panpf.sketch.decode.DecodeConfig +import com.github.panpf.sketch.decode.ImageInfo +import com.github.panpf.sketch.decode.ImageInvalidException +import com.github.panpf.sketch.decode.internal.ExifOrientationHelper +import com.github.panpf.sketch.decode.internal.checkImageInfo +import com.github.panpf.sketch.decode.toBitmapOptions +import com.github.panpf.sketch.source.DataSource +import com.github.panpf.sketch.source.FileDataSource +import com.github.panpf.sketch.util.Rect +import com.github.panpf.sketch.util.Size +import com.github.panpf.sketch.util.toAndroidRect +import java.io.IOException + +/** + * Decode encrypted bitmap using BitmapFactory + */ +@Throws(IOException::class) +fun DataSource.decodeEncryptedBitmap( + keychainHolder: KeychainHolder, + config: DecodeConfig? = null, + exifOrientationHelper: ExifOrientationHelper? = null +): Bitmap { + return with(this as FileDataSource) { + val options = config?.toBitmapOptions() + val encryptedFile = path.toFile() + val encryptedMedia = with(keychainHolder) { + encryptedFile.decrypt() + } + val bitmap = BitmapFactory.decodeByteArray( + encryptedMedia.bytes, + 0, + encryptedMedia.bytes.size, + options.apply { this?.outMimeType = encryptedMedia.mimeType } + ) ?: throw ImageInvalidException("decode return null at decodeEncryptedBitmap") + val exifOrientationHelper1 = + exifOrientationHelper ?: ExifOrientationHelper(readEncryptedExifOrientation(keychainHolder)) + exifOrientationHelper1.applyToBitmap(bitmap) ?: bitmap + } +} + +/** + * Use EncryptedBitmapRegionDecoder to decode part of a bitmap region + */ +@Throws(IOException::class) +fun DataSource.decodeEncryptedRegionBitmap( + keychainHolder: KeychainHolder, + srcRect: Rect, + config: DecodeConfig? = null, + imageSize: Size? = null, + exifOrientationHelper: ExifOrientationHelper? = null +): Bitmap { + return with(this as FileDataSource) { + val encryptedFile = path.toFile() + val encryptedMedia = with(keychainHolder) { + encryptedFile.decrypt() + } + val regionDecoder = if (VERSION.SDK_INT >= VERSION_CODES.S) { + BitmapRegionDecoder.newInstance(encryptedMedia.bytes, 0, encryptedMedia.bytes.size) + } else { + @Suppress("DEPRECATION") + BitmapRegionDecoder.newInstance(encryptedMedia.bytes, 0, encryptedMedia.bytes.size, false) + } + val imageSize1 = imageSize ?: readEncryptedImageInfo(keychainHolder, exifOrientationHelper).size + val exifOrientationHelper1 = + exifOrientationHelper ?: ExifOrientationHelper(readEncryptedExifOrientation(keychainHolder)) + val originalRegion = exifOrientationHelper1.applyToRect( + srcRect = srcRect, + spaceSize = imageSize1, + reverse = true + ) + val bitmapOptions = config?.toBitmapOptions() + val regionBitmap = try { + regionDecoder.decodeRegion(originalRegion.toAndroidRect(), bitmapOptions) + ?: throw ImageInvalidException("decode return null at decodeEncryptedRegionBitmap") + } finally { + regionDecoder.recycle() + } + + val correctedRegionImage = exifOrientationHelper1.applyToBitmap(regionBitmap) ?: regionBitmap + correctedRegionImage + } +} + +@Throws(IOException::class) +fun DataSource.readEncryptedExifOrientation(keychainHolder: KeychainHolder): Int { + return with(this as FileDataSource) { + val encryptedFile = path.toFile() + val encryptedMedia = with(keychainHolder) { + encryptedFile.decrypt() + } + encryptedMedia.bytes.inputStream().use { + ExifInterface(it).getAttributeInt( + ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_UNDEFINED + ) + } + } +} + +@Throws(IOException::class) +fun DataSource.readEncryptedImageInfoWithIgnoreExifOrientation(keychainHolder: KeychainHolder): ImageInfo { + with(this as FileDataSource) { + val boundOptions = BitmapFactory.Options().apply { + inJustDecodeBounds = true + } + val encryptedFile = path.toFile() + val encryptedMedia = with(keychainHolder) { + encryptedFile.decrypt() + } + try { + BitmapFactory.decodeByteArray( + encryptedMedia.bytes, + 0, + encryptedMedia.bytes.size, + boundOptions + ) + } catch (e: Exception) { + e.printStackTrace() + throw ImageInvalidException("decode return null at readEncryptedImageInfoWithIgnoreExifOrientation") + } + + val imageSize = Size(width = boundOptions.outWidth, height = boundOptions.outHeight) + return ImageInfo(size = imageSize, mimeType = encryptedMedia.mimeType) + .apply { checkImageInfo(this) } + } +} + +/** + * Read image information using BitmapFactory. Parse Exif orientation + */ +fun DataSource.readEncryptedImageInfo(keychainHolder: KeychainHolder, helper: ExifOrientationHelper?): ImageInfo { + val imageInfo = readEncryptedImageInfoWithIgnoreExifOrientation(keychainHolder) + val exifOrientationHelper = if (helper != null) { + helper + } else { + val exifOrientation = readEncryptedExifOrientationWithMimeType(keychainHolder, imageInfo.mimeType) + ExifOrientationHelper(exifOrientation) + } + val correctedImageSize = exifOrientationHelper.applyToSize(imageInfo.size) + return imageInfo.copy(size = correctedImageSize) +} + +/** + * Read the Exif orientation attribute of the image, if the mimeType is not supported, return [ExifInterface.ORIENTATION_UNDEFINED] + */ +@Throws(IOException::class) +fun DataSource.readEncryptedExifOrientationWithMimeType(keychainHolder: KeychainHolder, mimeType: String): Int = + if (ExifInterface.isSupportedMimeType(mimeType)) { + readEncryptedExifOrientation(keychainHolder) + } else { + ExifInterface.ORIENTATION_UNDEFINED + } + +/** + * Decode image width, height, MIME type. Parse Exif orientation + */ +fun DataSource.readEncryptedImageInfo(keychainHolder: KeychainHolder): ImageInfo = readEncryptedImageInfo(keychainHolder,null) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedRegionDecoder.kt b/app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedRegionDecoder.kt new file mode 100644 index 0000000000..4fdad8bac4 --- /dev/null +++ b/app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedRegionDecoder.kt @@ -0,0 +1,139 @@ +package com.dot.gallery.core.decoder + +import android.graphics.BitmapFactory +import android.graphics.BitmapRegionDecoder +import android.graphics.Rect +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES +import com.dot.gallery.feature_node.data.data_source.KeychainHolder +import com.dot.gallery.feature_node.domain.model.EncryptedMedia +import com.github.panpf.zoomimage.subsampling.BitmapTileImage +import com.github.panpf.zoomimage.subsampling.FileImageSource +import com.github.panpf.zoomimage.subsampling.ImageInfo +import com.github.panpf.zoomimage.subsampling.ImageSource +import com.github.panpf.zoomimage.subsampling.RegionDecoder +import com.github.panpf.zoomimage.subsampling.SubsamplingImage +import com.github.panpf.zoomimage.subsampling.TileImage +import com.github.panpf.zoomimage.subsampling.internal.ExifOrientationHelper +import com.github.panpf.zoomimage.util.IntRectCompat + +class EncryptedRegionDecoder( + override val subsamplingImage: SubsamplingImage, + val imageSource: ImageSource, + private val keychainHolder: KeychainHolder, +) : RegionDecoder { + + private var bitmapRegionDecoder: BitmapRegionDecoder? = null + + private val exifOrientationHelper: ExifOrientationHelper by lazy { + val exifOrientation = imageSource.readEncryptedExifOrientation(keychainHolder) + ExifOrientationHelper(exifOrientation) + } + + override val imageInfo: ImageInfo by lazy { imageSource.readEncryptedImageInfo(keychainHolder) } + + override fun close() { + bitmapRegionDecoder?.recycle() + } + + override fun copy(): RegionDecoder { + return EncryptedRegionDecoder( + subsamplingImage = subsamplingImage, + imageSource = imageSource, + keychainHolder = keychainHolder + ) + } + + override fun decodeRegion(key: String, region: IntRectCompat, sampleSize: Int): TileImage { + prepare() + val options = BitmapFactory.Options().apply { + inSampleSize = sampleSize + } + val originalRegion = exifOrientationHelper + .applyToRect(region, imageInfo.size, reverse = true) + val bitmap = bitmapRegionDecoder!!.decodeRegion(originalRegion.toAndroidRect(), options) + val tileImage = BitmapTileImage(bitmap, key, fromCache = false) + val correctedImage = exifOrientationHelper.applyToTileImage(tileImage) + return correctedImage + } + + override fun prepare() { + if (bitmapRegionDecoder != null) return + + val encryptedMedia = with(keychainHolder) { + (imageSource as FileImageSource).path.toFile().decrypt() + } + + bitmapRegionDecoder = kotlin.runCatching { + if (VERSION.SDK_INT >= VERSION_CODES.S) { + BitmapRegionDecoder.newInstance(encryptedMedia.bytes, 0, encryptedMedia.bytes.size) + } else { + @Suppress("DEPRECATION") + BitmapRegionDecoder.newInstance(encryptedMedia.bytes, 0, encryptedMedia.bytes.size, false) + } + }.apply { + if (isFailure) { + throw exceptionOrNull()!! + } + }.getOrThrow() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + other as EncryptedRegionDecoder + if (subsamplingImage != other.subsamplingImage) return false + if (imageSource != other.imageSource) return false + return true + } + + override fun hashCode(): Int { + var result = subsamplingImage.hashCode() + result = 31 * result + imageSource.hashCode() + return result + } + + override fun toString(): String { + return "EncryptedRegionDecoder(subsamplingImage=$subsamplingImage, imageSource=$imageSource)" + } + + private fun IntRectCompat.toAndroidRect(): Rect { + return Rect(left, top, right, bottom) + } + + class Factory(private val keychainHolder: KeychainHolder) : RegionDecoder.Factory { + + override suspend fun accept(subsamplingImage: SubsamplingImage): Boolean = true + + override fun checkSupport(mimeType: String): Boolean? = when (mimeType) { + "image/jpeg", "image/png", "image/webp" -> true + "image/gif", "image/bmp", "image/svg+xml" -> false + "image/heic", "image/heif" -> true + "image/avif" -> if (VERSION.SDK_INT <= 34) false else null + else -> null + } + + override fun create( + subsamplingImage: SubsamplingImage, + imageSource: ImageSource, + ): EncryptedRegionDecoder = EncryptedRegionDecoder( + subsamplingImage = subsamplingImage, + imageSource = imageSource, + keychainHolder = keychainHolder + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + return other != null && this::class == other::class + } + + override fun hashCode(): Int { + return this::class.hashCode() + } + + override fun toString(): String { + return "EncryptedRegionDecoder" + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedRegionDecoderExt.kt b/app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedRegionDecoderExt.kt new file mode 100644 index 0000000000..18d2d9bb5d --- /dev/null +++ b/app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedRegionDecoderExt.kt @@ -0,0 +1,108 @@ +package com.dot.gallery.core.decoder + +import android.graphics.BitmapFactory +import androidx.exifinterface.media.ExifInterface +import com.dot.gallery.feature_node.data.data_source.KeychainHolder +import com.dot.gallery.feature_node.domain.model.EncryptedMedia +import com.github.panpf.sketch.decode.ImageInvalidException +import com.github.panpf.sketch.decode.internal.ExifOrientationHelper +import com.github.panpf.sketch.util.Size +import com.github.panpf.zoomimage.subsampling.FileImageSource +import com.github.panpf.zoomimage.subsampling.ImageInfo +import com.github.panpf.zoomimage.subsampling.ImageSource +import com.github.panpf.zoomimage.util.IntSizeCompat +import com.github.panpf.zoomimage.util.isEmpty +import java.io.IOException + +@Throws(IOException::class) +fun ImageSource.readEncryptedExifOrientation(keychainHolder: KeychainHolder): Int { + return with(this as FileImageSource) { + val encryptedFile = path.toFile() + val encryptedMedia = with(keychainHolder) { + encryptedFile.decrypt() + } + encryptedMedia.bytes.inputStream().use { + ExifInterface(it).getAttributeInt( + ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_UNDEFINED + ) + } + } +} + +@Throws(IOException::class) +fun ImageSource.readEncryptedImageInfoWithIgnoreExifOrientation(keychainHolder: KeychainHolder): ImageInfo { + with(this as FileImageSource) { + val boundOptions = BitmapFactory.Options().apply { + inJustDecodeBounds = true + } + val encryptedFile = path.toFile() + val encryptedMedia = with(keychainHolder) { + encryptedFile.decrypt() + } + try { + BitmapFactory.decodeByteArray( + encryptedMedia.bytes, + 0, + encryptedMedia.bytes.size, + boundOptions.apply { this.outMimeType = encryptedMedia.mimeType } + ) + } catch (e: Exception) { + e.printStackTrace() + throw ImageInvalidException("decode return null at readEncryptedImageInfoWithIgnoreExifOrientation") + } + + val mimeType = encryptedMedia.mimeType + val imageSize = IntSizeCompat(width = boundOptions.outWidth, height = boundOptions.outHeight) + return ImageInfo(size = imageSize, mimeType = mimeType) + .apply { checkImageInfo(this) } + } +} + +/** + * Check if the image is valid + */ +fun checkImageSize(imageSize: IntSizeCompat) { + if (imageSize.isEmpty()) { + throw ImageInvalidException("Invalid image size. size=$imageSize") + } +} + +/** + * Check if the image is valid + */ +fun checkImageInfo(imageInfo: ImageInfo) { + checkImageSize(imageInfo.size) +} + + /** + * Read image information using BitmapFactory. Parse Exif orientation + */ +fun ImageSource.readEncryptedImageInfo(keychainHolder: KeychainHolder, helper: ExifOrientationHelper?): ImageInfo { + val imageInfo = readEncryptedImageInfoWithIgnoreExifOrientation(keychainHolder) + val exifOrientationHelper = if (helper != null) { + helper + } else { + val exifOrientation = readEncryptedExifOrientationWithMimeType(keychainHolder, imageInfo.mimeType) + ExifOrientationHelper(exifOrientation) + } + val correctedImageSize = exifOrientationHelper.applyToSize(Size(width = imageInfo.size.width, height = imageInfo.size.height)) + return imageInfo.copy(size = correctedImageSize.toIntSizedCompat()) +} + fun Size.toIntSizedCompat() = IntSizeCompat(width = width, height = height) + +/** + * Read the Exif orientation attribute of the image, if the mimeType is not supported, return [ExifInterface.ORIENTATION_UNDEFINED] + */ +@Throws(IOException::class) +fun ImageSource.readEncryptedExifOrientationWithMimeType(keychainHolder: KeychainHolder, mimeType: String): Int = + if (ExifInterface.isSupportedMimeType(mimeType)) { + readEncryptedExifOrientation(keychainHolder) + } else { + ExifInterface.ORIENTATION_UNDEFINED + } + +/** + * Decode image width, height, MIME type. Parse Exif orientation + */ +fun ImageSource.readEncryptedImageInfo(keychainHolder: KeychainHolder): ImageInfo = readEncryptedImageInfo(keychainHolder,null) diff --git a/app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedVideoFrameDecoder.kt b/app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedVideoFrameDecoder.kt index e12a5dbe61..b3ec55b695 100644 --- a/app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedVideoFrameDecoder.kt +++ b/app/src/main/kotlin/com/dot/gallery/core/decoder/EncryptedVideoFrameDecoder.kt @@ -7,15 +7,15 @@ import com.dot.gallery.feature_node.data.data_source.KeychainHolder import com.dot.gallery.feature_node.domain.model.EncryptedMedia import com.github.panpf.sketch.Image import com.github.panpf.sketch.Sketch -import com.github.panpf.sketch.asSketchImage +import com.github.panpf.sketch.asImage +import com.github.panpf.sketch.decode.DecodeConfig import com.github.panpf.sketch.decode.DecodeException import com.github.panpf.sketch.decode.Decoder import com.github.panpf.sketch.decode.ImageInfo -import com.github.panpf.sketch.decode.ImageInvalidException import com.github.panpf.sketch.decode.internal.DecodeHelper import com.github.panpf.sketch.decode.internal.ExifOrientationHelper import com.github.panpf.sketch.decode.internal.HelperDecoder -import com.github.panpf.sketch.decode.internal.newDecodeConfigByQualityParams +import com.github.panpf.sketch.decode.internal.checkImageInfo import com.github.panpf.sketch.fetch.FetchResult import com.github.panpf.sketch.request.ImageRequest import com.github.panpf.sketch.request.RequestContext @@ -29,9 +29,9 @@ import com.github.panpf.sketch.source.FileDataSource import com.github.panpf.sketch.source.getFileOrNull import com.github.panpf.sketch.util.Rect import com.github.panpf.sketch.util.Size +import com.github.panpf.sketch.util.div import java.io.File import java.io.FileOutputStream -import kotlin.math.roundToInt class EncryptedVideoFrameDecoder( private val requestContext: RequestContext, @@ -128,10 +128,6 @@ private class EncryptedVideoFrameDecodeHelper( } override fun decode(sampleSize: Int): Image { - val config = request.newDecodeConfigByQualityParams(imageInfo.mimeType).apply { - inSampleSize = sampleSize - } - val option = request.videoFrameOption ?: MediaMetadataRetriever.OPTION_CLOSEST_SYNC val frameMicros = request.videoFrameMicros ?: request.videoFramePercent?.let { percentDuration -> val duration = mediaMetadataRetriever @@ -140,36 +136,29 @@ private class EncryptedVideoFrameDecodeHelper( (duration * percentDuration * 1000).toLong() } ?: 0L - - val inSampleSize = config.inSampleSize?.toFloat() - val dstWidth = if (inSampleSize != null) { - (imageInfo.width / inSampleSize).roundToInt() - } else { - imageInfo.width - } - val dstHeight = if (inSampleSize != null) { - (imageInfo.height / inSampleSize).roundToInt() - } else { - imageInfo.height - } + val option = request.videoFrameOption ?: MediaMetadataRetriever.OPTION_CLOSEST_SYNC + val imageSize = imageInfo.size + val dstSize = imageSize / sampleSize.toFloat() + val config = DecodeConfig(request, imageInfo.mimeType, isOpaque = false) val bitmapParams = BitmapParams().apply { - val inPreferredConfigFromRequest = config.inPreferredConfig - if (inPreferredConfigFromRequest != null) { - preferredConfig = inPreferredConfigFromRequest - } + config.colorType?.also { preferredConfig = it } } - val bitmap = mediaMetadataRetriever - .getScaledFrameAtTime(frameMicros, option, dstWidth, dstHeight, bitmapParams) - ?: throw DecodeException( - "Failed to getScaledFrameAtTime. frameMicros=%d, option=%s, dst=%dx%d, image=%dx%d, preferredConfig=%s.".format( - frameMicros, optionToName(option), dstWidth, dstHeight, - imageInfo.width, imageInfo.height, config.inPreferredConfig - ) - ) - - val image = bitmap.asSketchImage() - val correctedImage = exifOrientationHelper.applyToImage(image) ?: image - return correctedImage + val bitmap = mediaMetadataRetriever.getScaledFrameAtTime( + /* timeUs = */ frameMicros, + /* option = */ option, + /* dstWidth = */ dstSize.width, + /* dstHeight = */ dstSize.height, + /* params = */ bitmapParams + ) ?: throw DecodeException( + "Failed to getScaledFrameAtTime. " + + "frameMicros=$frameMicros, " + + "option=${optionToName(option)}, " + + "dstSize=$dstSize, " + + "imageSize=$imageSize, " + + "preferredConfig=${config.colorType}" + ) + val correctedBitmap = exifOrientationHelper.applyToBitmap(bitmap) ?: bitmap + return correctedBitmap.asImage() } override fun decodeRegion(region: Rect, sampleSize: Int): Image { @@ -181,13 +170,10 @@ private class EncryptedVideoFrameDecodeHelper( .extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toIntOrNull() ?: 0 val srcHeight = mediaMetadataRetriever .extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull() ?: 0 - if (srcWidth <= 1 || srcHeight <= 1) { - val message = "Invalid video file. size=${srcWidth}x${srcHeight}" - throw ImageInvalidException(message) - } val imageSize = Size(width = srcWidth, height = srcHeight) val correctedImageSize = exifOrientationHelper.applyToSize(imageSize) return ImageInfo(size = correctedImageSize, mimeType = mimeType) + .apply { checkImageInfo(this) } } private fun readExifOrientation(): Int { diff --git a/app/src/main/kotlin/com/dot/gallery/core/decoder/SketchHeifDecoder.kt b/app/src/main/kotlin/com/dot/gallery/core/decoder/SketchHeifDecoder.kt index ff87f9c47f..c2b27f6db2 100644 --- a/app/src/main/kotlin/com/dot/gallery/core/decoder/SketchHeifDecoder.kt +++ b/app/src/main/kotlin/com/dot/gallery/core/decoder/SketchHeifDecoder.kt @@ -3,6 +3,7 @@ package com.dot.gallery.core.decoder import com.github.panpf.sketch.ComponentRegistry import com.github.panpf.sketch.decode.DecodeResult import com.github.panpf.sketch.decode.Decoder +import com.github.panpf.sketch.decode.ImageInfo import com.github.panpf.sketch.fetch.FetchResult import com.github.panpf.sketch.request.RequestContext import com.github.panpf.sketch.source.DataSource @@ -19,6 +20,8 @@ class SketchHeifDecoder( private val mimeType: String ) : Decoder { + private val coder = HeifCoder(requestContext.request.context) + class Factory : Decoder.Factory { override val key: String @@ -55,9 +58,8 @@ class SketchHeifDecoder( } } - override suspend fun decode(): Result = runCatching { - val coder = HeifCoder(requestContext.request.context) - return@runCatching dataSource.withCustomDecoder( + override fun decode(): DecodeResult { + return dataSource.withCustomDecoder( requestContext = requestContext, mimeType = mimeType, getSize = coder::getSize, @@ -65,4 +67,12 @@ class SketchHeifDecoder( ) } + override val imageInfo: ImageInfo by lazy { + dataSource.getImageInfo( + requestContext = requestContext, + mimeType = mimeType, + getSize = coder::getSize + ) + } + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/dot/gallery/core/decoder/SketchJxlDecoder.kt b/app/src/main/kotlin/com/dot/gallery/core/decoder/SketchJxlDecoder.kt index 071fb20f00..aab52aac04 100644 --- a/app/src/main/kotlin/com/dot/gallery/core/decoder/SketchJxlDecoder.kt +++ b/app/src/main/kotlin/com/dot/gallery/core/decoder/SketchJxlDecoder.kt @@ -5,6 +5,7 @@ import com.dot.gallery.core.decoder.SketchJxlDecoder.Factory.Companion.JXL_MIMET import com.github.panpf.sketch.ComponentRegistry import com.github.panpf.sketch.decode.DecodeResult import com.github.panpf.sketch.decode.Decoder +import com.github.panpf.sketch.decode.ImageInfo import com.github.panpf.sketch.fetch.FetchResult import com.github.panpf.sketch.request.RequestContext import com.github.panpf.sketch.source.DataSource @@ -48,8 +49,8 @@ class SketchJxlDecoder( } - override suspend fun decode(): Result = kotlin.runCatching { - return@runCatching dataSource.withCustomDecoder( + override fun decode(): DecodeResult { + return dataSource.withCustomDecoder( requestContext = requestContext, mimeType = JXL_MIMETYPE, getSize = JxlCoder::getSize, @@ -57,4 +58,12 @@ class SketchJxlDecoder( ) } + override val imageInfo: ImageInfo by lazy { + dataSource.getImageInfo( + requestContext = requestContext, + mimeType = JXL_MIMETYPE, + getSize = JxlCoder::getSize + ) + } + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/dot/gallery/core/decoder/ThumbnailDecoder.kt b/app/src/main/kotlin/com/dot/gallery/core/decoder/ThumbnailDecoder.kt deleted file mode 100644 index 49bb84e8bb..0000000000 --- a/app/src/main/kotlin/com/dot/gallery/core/decoder/ThumbnailDecoder.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.dot.gallery.core.decoder - -import com.dot.gallery.core.decoder.SketchHeifDecoder.Factory.Companion.HEIF_MIMETYPES -import com.dot.gallery.core.decoder.SketchJxlDecoder.Factory.Companion.JXL_MIMETYPE -import com.github.panpf.sketch.ComponentRegistry -import com.github.panpf.sketch.asSketchImage -import com.github.panpf.sketch.decode.DecodeResult -import com.github.panpf.sketch.decode.Decoder -import com.github.panpf.sketch.decode.ImageInfo -import com.github.panpf.sketch.decode.internal.calculateSampleSize -import com.github.panpf.sketch.decode.internal.createInSampledTransformed -import com.github.panpf.sketch.decode.internal.isSmallerSizeMode -import com.github.panpf.sketch.fetch.FetchResult -import com.github.panpf.sketch.request.RequestContext -import com.github.panpf.sketch.source.ContentDataSource -import com.github.panpf.sketch.source.DataSource -import com.github.panpf.sketch.util.MimeTypeMap -import com.github.panpf.sketch.util.Size - - -fun ComponentRegistry.Builder.supportThumbnailDecoder(): ComponentRegistry.Builder = apply { - addDecoder(ThumbnailDecoder.Factory()) -} - -class ThumbnailDecoder( - private val requestContext: RequestContext, - private val dataSource: DataSource, -) : Decoder { - - class Factory : Decoder.Factory { - - override val key: String - get() = "ThumbnailDecoder" - - override fun create(requestContext: RequestContext, fetchResult: FetchResult): Decoder? { - val mimeType = fetchResult.mimeType - return if ( - mimeType != null && - mimeType.isVideoOrImage && - !isSvg(fetchResult) && - !isSpecialFormat(fetchResult) && - fetchResult.dataSource is ContentDataSource - ) - ThumbnailDecoder(requestContext, fetchResult.dataSource) - else null - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - return other is Factory - } - - override fun hashCode(): Int { - return this@Factory::class.hashCode() - } - - override fun toString(): String = key - - private val String.isVideoOrImage get() = startsWith("video/") || startsWith("image/") - - private fun isSvg(result: FetchResult) = - result.mimeType?.contains(MIME_TYPE_SVG) == true - - private fun isSpecialFormat(result: FetchResult) = - HEIF_MIMETYPES.any { result.mimeType?.contains(it) == true } || result.mimeType?.contains(JXL_MIMETYPE) == true - - companion object { - private const val MIME_TYPE_SVG = "image/svg" - } - } - - override suspend fun decode(): Result = runCatching { - val request = requestContext.request - val dataSource = (dataSource as ContentDataSource) - - val size = requestContext.size - val bitmap = request.context.contentResolver.loadThumbnail( - dataSource.contentUri, - android.util.Size(size!!.width, size.height), - null - ) - val mimeType = MimeTypeMap.getMimeTypeFromUrl(dataSource.contentUri.toString()).toString() - val imageSize = Size(bitmap.width, bitmap.height) - val precision = request.precisionDecider.get( - imageSize = imageSize, - targetSize = size, - ) - val inSampleSize = calculateSampleSize( - imageSize = imageSize, - targetSize = requestContext.size!!, - smallerSizeMode = precision.isSmallerSizeMode() - ) - - DecodeResult( - image = bitmap.asSketchImage(), - imageInfo = ImageInfo( - width = bitmap.width, - height = bitmap.height, - mimeType = mimeType - ), - dataFrom = dataSource.dataFrom, - resize = requestContext.computeResize(requestContext.size!!), - transformeds = if (inSampleSize != 1) listOf(createInSampledTransformed(inSampleSize)) else null, - extras = null - ) - } - -} diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/DecryptedMedia.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/DecryptedMedia.kt index ec738a4521..409a664fe0 100644 --- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/DecryptedMedia.kt +++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/DecryptedMedia.kt @@ -4,6 +4,9 @@ import android.graphics.Bitmap import android.net.Uri import android.os.Parcelable import androidx.compose.runtime.Stable +import androidx.core.net.toFile +import com.github.panpf.zoomimage.subsampling.FileImageSource +import com.github.panpf.zoomimage.subsampling.SubsamplingImage import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import java.io.Serializable @@ -89,6 +92,10 @@ data class DecryptedMedia( } +fun DecryptedMedia.asSubsamplingImage(): SubsamplingImage { + return SubsamplingImage(imageSource = FileImageSource(Uri.parse(uri).toFile())) +} + fun DecryptedMedia.compatibleMimeType(): String { return if (isImage) when(mimeType) { "image/jpeg" -> "image/jpeg" diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/EditViewModel.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/EditViewModel.kt index 742bcc2be1..2b0ff0c52a 100644 --- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/EditViewModel.kt +++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/EditViewModel.kt @@ -26,7 +26,7 @@ import com.dot.gallery.feature_node.presentation.edit.adjustments.varfilter.Vari import com.dot.gallery.feature_node.presentation.util.overlayBitmaps import com.dot.gallery.feature_node.presentation.util.printDebug import com.dot.gallery.feature_node.presentation.util.printError -import com.github.panpf.sketch.getBitmapOrNull +import com.github.panpf.sketch.BitmapImage import com.github.panpf.sketch.request.ImageRequest import com.github.panpf.sketch.sketch import com.github.panpf.sketch.util.Size @@ -187,7 +187,7 @@ class EditViewModel @Inject constructor( ) } val result = context.sketch.execute(request) - val bitmap = result.image?.getBitmapOrNull() + val bitmap = (result.image as? BitmapImage)?.bitmap _originalBitmap.value = bitmap _targetBitmap.value = bitmap if (_currentBitmap.value == null) { diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/components/markup/MarkupPainter.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/components/markup/MarkupPainter.kt index 4688e541ba..66a9feeafb 100644 --- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/components/markup/MarkupPainter.kt +++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/components/markup/MarkupPainter.kt @@ -32,14 +32,12 @@ import com.dot.gallery.feature_node.domain.model.editor.PainterMotionEvent import com.dot.gallery.feature_node.domain.model.editor.PathProperties import com.dot.gallery.feature_node.presentation.edit.utils.dragMotionEvent import com.dot.gallery.feature_node.presentation.util.goBack -import io.ktor.util.InternalAPI import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -@OptIn(InternalAPI::class) @Composable fun MarkupPainter( modifier: Modifier = Modifier, diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/MediaViewScreen.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/MediaViewScreen.kt index 45807cee69..55ca0c104d 100644 --- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/MediaViewScreen.kt +++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/MediaViewScreen.kt @@ -87,7 +87,7 @@ import com.dot.gallery.feature_node.presentation.util.rememberWindowInsetsContro import com.dot.gallery.feature_node.presentation.util.setHdrMode import com.dot.gallery.feature_node.presentation.util.toggleSystemBars import com.dot.gallery.ui.theme.BlackScrim -import com.github.panpf.sketch.getBitmapOrNull +import com.github.panpf.sketch.BitmapImage import com.github.panpf.sketch.request.ImageRequest import com.github.panpf.sketch.sketch import kotlinx.coroutines.delay @@ -193,7 +193,7 @@ fun MediaViewScreen( ) } val result = context.sketch.execute(request) - result.image?.getBitmapOrNull()?.let { bitmap -> + (result.image as? BitmapImage)?.bitmap?.let { bitmap -> context.setHdrMode(bitmap.hasGainmap()) } } diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/vault/encryptedmediaview/components/media/ZoomablePagerImage.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/vault/encryptedmediaview/components/media/ZoomablePagerImage.kt index 7493be0255..3a84b6affb 100644 --- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/vault/encryptedmediaview/components/media/ZoomablePagerImage.kt +++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/vault/encryptedmediaview/components/media/ZoomablePagerImage.kt @@ -12,23 +12,30 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.blur import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.dot.gallery.core.Constants.DEFAULT_TOP_BAR_ANIMATION_DURATION import com.dot.gallery.core.Settings.Misc.rememberAllowBlur +import com.dot.gallery.core.decoder.EncryptedRegionDecoder import com.dot.gallery.core.presentation.components.util.LocalBatteryStatus import com.dot.gallery.core.presentation.components.util.ProvideBatteryStatus import com.dot.gallery.core.presentation.components.util.swipe +import com.dot.gallery.feature_node.data.data_source.KeychainHolder import com.dot.gallery.feature_node.domain.model.DecryptedMedia +import com.dot.gallery.feature_node.domain.model.asSubsamplingImage import com.github.panpf.sketch.cache.CachePolicy import com.github.panpf.sketch.rememberAsyncImagePainter import com.github.panpf.sketch.request.ComposableImageRequest import com.github.panpf.zoomimage.ZoomImage +import com.github.panpf.zoomimage.compose.rememberZoomState @Composable fun ZoomablePagerImage( @@ -40,7 +47,7 @@ fun ZoomablePagerImage( ) { val painter = rememberAsyncImagePainter( request = ComposableImageRequest(media.uri) { - memoryCachePolicy(CachePolicy.ENABLED) + memoryCachePolicy(CachePolicy.DISABLED) crossfade() setExtra( key = "encryptedMediaKey", @@ -74,6 +81,17 @@ fun ZoomablePagerImage( } } + val zoomState = rememberZoomState() + val context = LocalContext.current + val keychainHolder = remember(context) { + KeychainHolder(context) + } + + LaunchedEffect(zoomState.subsampling) { + zoomState.subsampling.regionDecoders = listOf(EncryptedRegionDecoder.Factory(keychainHolder)) + zoomState.setSubsamplingImage(media.asSubsamplingImage()) + } + ZoomImage( modifier = modifier .fillMaxSize() @@ -82,10 +100,10 @@ fun ZoomablePagerImage( ), onTap = { onItemClick() }, painter = painter, + zoomState = zoomState, contentScale = ContentScale.Fit, contentDescription = media.label ) } - } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f7935f35d2..e72f6193ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,12 +36,12 @@ room = "2.6.1" accompanist = "0.36.0" datastore = "1.1.1" securityCrypto = "1.1.0-alpha06" -sketch = "4.0.0-alpha08" +sketch = "4.0.0-beta02" uiautomator = "2.3.0" composealpha = "1.8.0-alpha05" kotlinxSerializationJson = "1.7.3" workRuntimeKtx = "2.10.0" -zoomimageViewSketch = "1.1.0-beta01" +zoomimageViewSketch = "1.1.0-rc01" [libraries] # AndroidX @@ -116,12 +116,14 @@ accompanist-permissions = { group = "com.google.accompanist", name = "accompanis accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist" } # Zoomable -sketch-animated = { module = "io.github.panpf.sketch4:sketch-animated", version.ref = "sketch" } +sketch-animated-gif = { module = "io.github.panpf.sketch4:sketch-animated-gif", version.ref = "sketch" } +sketch-animated-heif = { module = "io.github.panpf.sketch4:sketch-animated-heif", version.ref = "sketch" } +sketch-animated-webp = { module = "io.github.panpf.sketch4:sketch-animated-webp", version.ref = "sketch" } sketch-compose = { module = "io.github.panpf.sketch4:sketch-compose", version.ref = "sketch" } sketch-extensions-compose = { module = "io.github.panpf.sketch4:sketch-extensions-compose", version.ref = "sketch" } -sketch-http-ktor = { module = "io.github.panpf.sketch4:sketch-http-ktor", version.ref = "sketch" } +sketch-http-ktor = { module = "io.github.panpf.sketch4:sketch-http-ktor3", version.ref = "sketch" } sketch-svg = { module = "io.github.panpf.sketch4:sketch-svg", version.ref = "sketch" } -sketch-video = { module = "io.github.panpf.sketch4:sketch-video-ffmpeg", version.ref = "sketch" } +sketch-video = { module = "io.github.panpf.sketch4:sketch-video", version.ref = "sketch" } sketch-view = { module = "io.github.panpf.sketch4:sketch-view", version.ref = "sketch" } # Kotlinx @@ -140,7 +142,7 @@ androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } -zoomimage-sketch = { module = "io.github.panpf.zoomimage:zoomimage-compose-sketch", version.ref = "zoomimageViewSketch" } +zoomimage-sketch = { module = "io.github.panpf.zoomimage:zoomimage-compose-sketch4", version.ref = "zoomimageViewSketch" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" }