@@ -16,8 +16,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
versionCode = 677
|
||||
versionName = '7.6.4'
|
||||
versionCode = 678
|
||||
versionName = '7.6.5'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||
ksp {
|
||||
@@ -92,7 +92,7 @@ dependencies {
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.core:core-ktx:1.13.1'
|
||||
implementation 'androidx.activity:activity-ktx:1.9.2'
|
||||
implementation 'androidx.activity:activity-ktx:1.9.3'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.8.4'
|
||||
implementation 'androidx.transition:transition-ktx:1.5.1'
|
||||
implementation 'androidx.collection:collection-ktx:1.4.4'
|
||||
@@ -136,7 +136,8 @@ dependencies {
|
||||
|
||||
implementation 'io.coil-kt:coil-base:2.7.0'
|
||||
implementation 'io.coil-kt:coil-svg:2.7.0'
|
||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:ac7360c5e3'
|
||||
implementation 'org.aomedia.avif.android:avif:1.1.1.14d8e3c4'
|
||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:d1d10a6975'
|
||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||
implementation 'io.noties.markwon:core:4.6.2'
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ import okhttp3.OkHttpClient
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.image.AvifImageDecoder
|
||||
import org.koitharu.kotatsu.core.image.RegionBitmapDecoder
|
||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
@@ -119,6 +121,8 @@ interface AppModule {
|
||||
ComponentRegistry.Builder()
|
||||
.add(SvgDecoder.Factory())
|
||||
.add(CbzFetcher.Factory())
|
||||
.add(AvifImageDecoder.Factory())
|
||||
.add(RegionBitmapDecoder.Factory())
|
||||
.add(FaviconFetcher.Factory(context, okHttpClientLazy, mangaRepositoryFactory))
|
||||
.add(MangaPageKeyer())
|
||||
.add(pageFetcherFactory)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.koitharu.kotatsu.core.image
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DecodeResult
|
||||
import coil.decode.Decoder
|
||||
import coil.decode.ImageSource
|
||||
import coil.fetch.SourceResult
|
||||
import coil.request.Options
|
||||
import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import org.aomedia.avif.android.AvifDecoder
|
||||
import org.aomedia.avif.android.AvifDecoder.Info
|
||||
import org.koitharu.kotatsu.core.util.ext.toByteBuffer
|
||||
|
||||
class AvifImageDecoder(source: ImageSource, options: Options, parallelismLock: Semaphore) :
|
||||
BaseCoilDecoder(source, options, parallelismLock) {
|
||||
|
||||
override fun BitmapFactory.Options.decode(): DecodeResult {
|
||||
val bytes = source.source().use {
|
||||
it.inputStream().toByteBuffer()
|
||||
}
|
||||
val info = Info()
|
||||
if (!AvifDecoder.getInfo(bytes, bytes.remaining(), info)) {
|
||||
throw ImageDecodeException(
|
||||
null,
|
||||
"avif",
|
||||
"Requested to decode byte buffer which cannot be handled by AvifDecoder",
|
||||
)
|
||||
}
|
||||
val config = if (info.depth == 8 || info.alphaPresent) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565
|
||||
val bitmap = Bitmap.createBitmap(info.width, info.height, config)
|
||||
if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) {
|
||||
bitmap.recycle()
|
||||
throw ImageDecodeException(null, "avif")
|
||||
}
|
||||
return DecodeResult(
|
||||
drawable = bitmap.toDrawable(options.context.resources),
|
||||
isSampled = false,
|
||||
)
|
||||
}
|
||||
|
||||
class Factory : Decoder.Factory {
|
||||
|
||||
private val parallelismLock = Semaphore(DEFAULT_PARALLELISM)
|
||||
|
||||
override fun create(
|
||||
result: SourceResult,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader
|
||||
): Decoder? = if (isApplicable(result)) {
|
||||
AvifImageDecoder(result.source, options, parallelismLock)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override fun equals(other: Any?) = other is Factory
|
||||
|
||||
override fun hashCode() = javaClass.hashCode()
|
||||
|
||||
private fun isApplicable(result: SourceResult): Boolean {
|
||||
return result.mimeType == "image/avif"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.koitharu.kotatsu.core.image
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import coil.decode.DecodeResult
|
||||
import coil.decode.Decoder
|
||||
import coil.decode.ImageSource
|
||||
import coil.request.Options
|
||||
import coil.size.Dimension
|
||||
import coil.size.Scale
|
||||
import coil.size.Size
|
||||
import coil.size.isOriginal
|
||||
import coil.size.pxOrElse
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import org.jetbrains.annotations.Blocking
|
||||
|
||||
abstract class BaseCoilDecoder(
|
||||
protected val source: ImageSource,
|
||||
protected val options: Options,
|
||||
private val parallelismLock: Semaphore,
|
||||
) : Decoder {
|
||||
|
||||
final override suspend fun decode(): DecodeResult = parallelismLock.withPermit {
|
||||
runInterruptible { BitmapFactory.Options().decode() }
|
||||
}
|
||||
|
||||
@Blocking
|
||||
protected abstract fun BitmapFactory.Options.decode(): DecodeResult
|
||||
|
||||
protected companion object {
|
||||
|
||||
const val DEFAULT_PARALLELISM = 4
|
||||
|
||||
inline fun Size.widthPx(scale: Scale, original: () -> Int): Int {
|
||||
return if (isOriginal) original() else width.toPx(scale)
|
||||
}
|
||||
|
||||
inline fun Size.heightPx(scale: Scale, original: () -> Int): Int {
|
||||
return if (isOriginal) original() else height.toPx(scale)
|
||||
}
|
||||
|
||||
fun Dimension.toPx(scale: Scale) = pxOrElse {
|
||||
when (scale) {
|
||||
Scale.FILL -> Int.MIN_VALUE
|
||||
Scale.FIT -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.koitharu.kotatsu.core.image
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.ImageDecoder
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import org.aomedia.avif.android.AvifDecoder
|
||||
import org.aomedia.avif.android.AvifDecoder.Info
|
||||
import org.jetbrains.annotations.Blocking
|
||||
import org.koitharu.kotatsu.core.util.ext.toByteBuffer
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.file.Files
|
||||
|
||||
object BitmapDecoderCompat {
|
||||
|
||||
private const val FORMAT_AVIF = "avif"
|
||||
|
||||
@Blocking
|
||||
fun decode(file: File): Bitmap = when (val format = getMimeType(file)?.subtype) {
|
||||
FORMAT_AVIF -> file.inputStream().use { decodeAvif(it.toByteBuffer()) }
|
||||
else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
ImageDecoder.decodeBitmap(ImageDecoder.createSource(file))
|
||||
} else {
|
||||
checkBitmapNotNull(BitmapFactory.decodeFile(file.absolutePath), format)
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
fun decode(stream: InputStream, type: MediaType?): Bitmap {
|
||||
val format = type?.subtype
|
||||
if (format == FORMAT_AVIF) {
|
||||
return decodeAvif(stream.toByteBuffer())
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
return checkBitmapNotNull(BitmapFactory.decodeStream(stream), format)
|
||||
}
|
||||
val byteBuffer = stream.toByteBuffer()
|
||||
return if (AvifDecoder.isAvifImage(byteBuffer)) {
|
||||
decodeAvif(byteBuffer)
|
||||
} else {
|
||||
ImageDecoder.decodeBitmap(ImageDecoder.createSource(byteBuffer))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMimeType(file: File): MediaType? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Files.probeContentType(file.toPath())?.toMediaTypeOrNull()
|
||||
} else {
|
||||
MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension)?.toMediaTypeOrNull()
|
||||
}
|
||||
|
||||
private fun checkBitmapNotNull(bitmap: Bitmap?, format: String?): Bitmap =
|
||||
bitmap ?: throw ImageDecodeException(null, format)
|
||||
|
||||
private fun decodeAvif(bytes: ByteBuffer): Bitmap {
|
||||
val info = Info()
|
||||
if (!AvifDecoder.getInfo(bytes, bytes.remaining(), info)) {
|
||||
throw ImageDecodeException(
|
||||
null,
|
||||
FORMAT_AVIF,
|
||||
"Requested to decode byte buffer which cannot be handled by AvifDecoder",
|
||||
)
|
||||
}
|
||||
val config = if (info.depth == 8 || info.alphaPresent) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565
|
||||
val bitmap = Bitmap.createBitmap(info.width, info.height, config)
|
||||
if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) {
|
||||
bitmap.recycle()
|
||||
throw ImageDecodeException(null, FORMAT_AVIF)
|
||||
}
|
||||
return bitmap
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.core.ui.image
|
||||
package org.koitharu.kotatsu.core.image
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
@@ -13,27 +13,14 @@ import coil.decode.Decoder
|
||||
import coil.decode.ImageSource
|
||||
import coil.fetch.SourceResult
|
||||
import coil.request.Options
|
||||
import coil.size.Dimension
|
||||
import coil.size.Scale
|
||||
import coil.size.Size
|
||||
import coil.size.isOriginal
|
||||
import coil.size.pxOrElse
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class RegionBitmapDecoder(
|
||||
private val source: ImageSource,
|
||||
private val options: Options,
|
||||
private val parallelismLock: Semaphore,
|
||||
) : Decoder {
|
||||
source: ImageSource, options: Options, parallelismLock: Semaphore
|
||||
) : BaseCoilDecoder(source, options, parallelismLock) {
|
||||
|
||||
override suspend fun decode() = parallelismLock.withPermit {
|
||||
runInterruptible { BitmapFactory.Options().decode() }
|
||||
}
|
||||
|
||||
private fun BitmapFactory.Options.decode(): DecodeResult {
|
||||
override fun BitmapFactory.Options.decode(): DecodeResult {
|
||||
val regionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
BitmapRegionDecoder.newInstance(source.source().inputStream())
|
||||
} else {
|
||||
@@ -55,29 +42,6 @@ class RegionBitmapDecoder(
|
||||
}
|
||||
}
|
||||
|
||||
private fun BitmapFactory.Options.configureConfig() {
|
||||
var config = options.config
|
||||
|
||||
inMutable = false
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 26 && options.colorSpace != null) {
|
||||
inPreferredColorSpace = options.colorSpace
|
||||
}
|
||||
inPremultiplied = options.premultipliedAlpha
|
||||
|
||||
// Decode the image as RGB_565 as an optimization if allowed.
|
||||
if (options.allowRgb565 && config == Bitmap.Config.ARGB_8888 && outMimeType == "image/jpeg") {
|
||||
config = Bitmap.Config.RGB_565
|
||||
}
|
||||
|
||||
// High color depth images must be decoded as either RGBA_F16 or HARDWARE.
|
||||
if (Build.VERSION.SDK_INT >= 26 && outConfig == Bitmap.Config.RGBA_F16 && config != Bitmap.Config.HARDWARE) {
|
||||
config = Bitmap.Config.RGBA_F16
|
||||
}
|
||||
|
||||
inPreferredConfig = config
|
||||
}
|
||||
|
||||
/** Compute and set the scaling properties for [BitmapFactory.Options]. */
|
||||
private fun BitmapFactory.Options.configureScale(srcWidth: Int, srcHeight: Int): Rect {
|
||||
val dstWidth = options.size.widthPx(options.scale) { srcWidth }
|
||||
@@ -142,18 +106,41 @@ class RegionBitmapDecoder(
|
||||
return rect
|
||||
}
|
||||
|
||||
class Factory(
|
||||
maxParallelism: Int = DEFAULT_MAX_PARALLELISM,
|
||||
) : Decoder.Factory {
|
||||
private fun BitmapFactory.Options.configureConfig() {
|
||||
var config = options.config
|
||||
|
||||
@Suppress("NEWER_VERSION_IN_SINCE_KOTLIN")
|
||||
@SinceKotlin("999.9") // Only public in Java.
|
||||
constructor() : this()
|
||||
inMutable = false
|
||||
|
||||
private val parallelismLock = Semaphore(maxParallelism)
|
||||
if (Build.VERSION.SDK_INT >= 26 && options.colorSpace != null) {
|
||||
inPreferredColorSpace = options.colorSpace
|
||||
}
|
||||
inPremultiplied = options.premultipliedAlpha
|
||||
|
||||
override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder {
|
||||
return RegionBitmapDecoder(result.source, options, parallelismLock)
|
||||
// Decode the image as RGB_565 as an optimization if allowed.
|
||||
if (options.allowRgb565 && config == Bitmap.Config.ARGB_8888 && outMimeType == "image/jpeg") {
|
||||
config = Bitmap.Config.RGB_565
|
||||
}
|
||||
|
||||
// High color depth images must be decoded as either RGBA_F16 or HARDWARE.
|
||||
if (Build.VERSION.SDK_INT >= 26 && outConfig == Bitmap.Config.RGBA_F16 && config != Bitmap.Config.HARDWARE) {
|
||||
config = Bitmap.Config.RGBA_F16
|
||||
}
|
||||
|
||||
inPreferredConfig = config
|
||||
}
|
||||
|
||||
class Factory : Decoder.Factory {
|
||||
|
||||
private val parallelismLock = Semaphore(DEFAULT_PARALLELISM)
|
||||
|
||||
override fun create(
|
||||
result: SourceResult,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader
|
||||
): Decoder? = if (options.parameters.value<Boolean>(PARAM_REGION) == true) {
|
||||
RegionBitmapDecoder(result.source, options, parallelismLock)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override fun equals(other: Any?) = other is Factory
|
||||
@@ -164,22 +151,7 @@ class RegionBitmapDecoder(
|
||||
companion object {
|
||||
|
||||
const val PARAM_SCROLL = "scroll"
|
||||
const val PARAM_REGION = "region"
|
||||
const val SCROLL_UNDEFINED = -1
|
||||
private const val DEFAULT_MAX_PARALLELISM = 4
|
||||
|
||||
private inline fun Size.widthPx(scale: Scale, original: () -> Int): Int {
|
||||
return if (isOriginal) original() else width.toPx(scale)
|
||||
}
|
||||
|
||||
private inline fun Size.heightPx(scale: Scale, original: () -> Int): Int {
|
||||
return if (isOriginal) original() else height.toPx(scale)
|
||||
}
|
||||
|
||||
private fun Dimension.toPx(scale: Scale) = pxOrElse {
|
||||
when (scale) {
|
||||
Scale.FILL -> Int.MIN_VALUE
|
||||
Scale.FIT -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@ import coil.request.SuccessResult
|
||||
import coil.util.CoilUtils
|
||||
import com.google.android.material.progressindicator.BaseProgressIndicator
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.image.RegionBitmapDecoder
|
||||
import org.koitharu.kotatsu.core.ui.image.AnimatedPlaceholderDrawable
|
||||
import org.koitharu.kotatsu.core.ui.image.RegionBitmapDecoder
|
||||
import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import com.google.android.material.R as materialR
|
||||
@@ -63,7 +63,7 @@ fun ImageRequest.Builder.indicator(indicators: List<BaseProgressIndicator<*>>):
|
||||
|
||||
fun ImageRequest.Builder.decodeRegion(
|
||||
scroll: Int = RegionBitmapDecoder.SCROLL_UNDEFINED,
|
||||
): ImageRequest.Builder = decoderFactory(RegionBitmapDecoder.Factory())
|
||||
): ImageRequest.Builder = setParameter(RegionBitmapDecoder.PARAM_REGION, true)
|
||||
.setParameter(RegionBitmapDecoder.PARAM_SCROLL, scroll)
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
|
||||
@@ -7,9 +7,12 @@ import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.storage.StorageManager
|
||||
import android.provider.OpenableColumns
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.database.getStringOrNull
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import org.jetbrains.annotations.Blocking
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.fs.FileSequence
|
||||
@@ -38,6 +41,12 @@ fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).use { output ->
|
||||
output.bufferedReader().use(BufferedReader::readText)
|
||||
}
|
||||
|
||||
val ZipEntry.mimeType: MediaType?
|
||||
get() {
|
||||
val ext = name.substringAfterLast('.')
|
||||
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)?.toMediaTypeOrNull()
|
||||
}
|
||||
|
||||
fun File.getStorageName(context: Context): String = runCatching {
|
||||
val manager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
|
||||
@@ -10,6 +10,9 @@ import okio.BufferedSink
|
||||
import okio.Source
|
||||
import org.koitharu.kotatsu.core.util.CancellableSource
|
||||
import org.koitharu.kotatsu.core.util.progress.ProgressResponseBody
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
fun ResponseBody.withProgress(progressState: MutableStateFlow<Float>): ResponseBody {
|
||||
return ProgressResponseBody(this, progressState)
|
||||
@@ -23,3 +26,10 @@ suspend fun Source.cancellable(): Source {
|
||||
suspend fun BufferedSink.writeAllCancellable(source: Source) = withContext(Dispatchers.IO) {
|
||||
writeAll(source.cancellable())
|
||||
}
|
||||
|
||||
fun InputStream.toByteBuffer(): ByteBuffer {
|
||||
val outStream = ByteArrayOutputStream(available())
|
||||
copyTo(outStream)
|
||||
val bytes = outStream.toByteArray()
|
||||
return ByteBuffer.allocateDirect(bytes.size).put(bytes).position(0) as ByteBuffer
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package org.koitharu.kotatsu.reader.domain
|
||||
|
||||
import android.content.ContentResolver.MimeTypeInfo
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.ImageDecoder
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.collection.set
|
||||
@@ -61,6 +59,8 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mimeType
|
||||
import org.koitharu.kotatsu.parsers.util.requireBody
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.core.image.BitmapDecoderCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.mimeType
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import java.util.LinkedList
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
@@ -144,8 +144,8 @@ class PageLoader @Inject constructor(
|
||||
ZipFile(uri.schemeSpecificPart).use { zip ->
|
||||
val entry = zip.getEntry(uri.fragment)
|
||||
context.ensureRamAtLeast(entry.size * 2)
|
||||
zip.getInputStream(zip.getEntry(uri.fragment)).use {
|
||||
checkBitmapNotNull(BitmapFactory.decodeStream(it))
|
||||
zip.getInputStream(entry).use {
|
||||
BitmapDecoderCompat.decode(it, entry.mimeType)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,11 +154,7 @@ class PageLoader @Inject constructor(
|
||||
val file = uri.toFile()
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
context.ensureRamAtLeast(file.length() * 2)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
ImageDecoder.decodeBitmap(ImageDecoder.createSource(file))
|
||||
} else {
|
||||
checkBitmapNotNull(BitmapFactory.decodeFile(file.absolutePath))
|
||||
}
|
||||
BitmapDecoderCompat.decode(file)
|
||||
}.use { image ->
|
||||
image.compressToPNG(file)
|
||||
}
|
||||
@@ -253,8 +249,6 @@ class PageLoader @Inject constructor(
|
||||
return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES)
|
||||
}
|
||||
|
||||
private fun checkBitmapNotNull(bitmap: Bitmap?): Bitmap = checkNotNull(bitmap) { "Cannot decode bitmap" }
|
||||
|
||||
private fun Deferred<Uri>.isValid(): Boolean {
|
||||
return getCompletionResultOrNull()?.map { uri ->
|
||||
uri.exists() && uri.isTargetNotEmpty()
|
||||
|
||||
Reference in New Issue
Block a user