Support for AVIF images
This commit is contained in:
@@ -16,8 +16,8 @@ android {
|
|||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 681
|
versionCode = 682
|
||||||
versionName = '7.7-a2'
|
versionName = '7.7-a3'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||||
ksp {
|
ksp {
|
||||||
@@ -92,7 +92,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||||
implementation 'androidx.core:core-ktx:1.13.1'
|
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.fragment:fragment-ktx:1.8.4'
|
||||||
implementation 'androidx.transition:transition-ktx:1.5.1'
|
implementation 'androidx.transition:transition-ktx:1.5.1'
|
||||||
implementation 'androidx.collection:collection-ktx:1.4.4'
|
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-base:2.7.0'
|
||||||
implementation 'io.coil-kt:coil-svg: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 'com.github.solkin:disk-lru-cache:1.4'
|
||||||
implementation 'io.noties.markwon:core:4.6.2'
|
implementation 'io.noties.markwon:core:4.6.2'
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import okhttp3.OkHttpClient
|
|||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
|
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
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.MangaHttpClient
|
||||||
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
|
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
|
||||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||||
@@ -119,6 +121,8 @@ interface AppModule {
|
|||||||
ComponentRegistry.Builder()
|
ComponentRegistry.Builder()
|
||||||
.add(SvgDecoder.Factory())
|
.add(SvgDecoder.Factory())
|
||||||
.add(CbzFetcher.Factory())
|
.add(CbzFetcher.Factory())
|
||||||
|
.add(AvifImageDecoder.Factory())
|
||||||
|
.add(RegionBitmapDecoder.Factory())
|
||||||
.add(FaviconFetcher.Factory(context, okHttpClientLazy, mangaRepositoryFactory))
|
.add(FaviconFetcher.Factory(context, okHttpClientLazy, mangaRepositoryFactory))
|
||||||
.add(MangaPageKeyer())
|
.add(MangaPageKeyer())
|
||||||
.add(pageFetcherFactory)
|
.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.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
@@ -13,27 +13,14 @@ import coil.decode.Decoder
|
|||||||
import coil.decode.ImageSource
|
import coil.decode.ImageSource
|
||||||
import coil.fetch.SourceResult
|
import coil.fetch.SourceResult
|
||||||
import coil.request.Options
|
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.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class RegionBitmapDecoder(
|
class RegionBitmapDecoder(
|
||||||
private val source: ImageSource,
|
source: ImageSource, options: Options, parallelismLock: Semaphore
|
||||||
private val options: Options,
|
) : BaseCoilDecoder(source, options, parallelismLock) {
|
||||||
private val parallelismLock: Semaphore,
|
|
||||||
) : Decoder {
|
|
||||||
|
|
||||||
override suspend fun decode() = parallelismLock.withPermit {
|
override fun BitmapFactory.Options.decode(): DecodeResult {
|
||||||
runInterruptible { BitmapFactory.Options().decode() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun BitmapFactory.Options.decode(): DecodeResult {
|
|
||||||
val regionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
val regionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
BitmapRegionDecoder.newInstance(source.source().inputStream())
|
BitmapRegionDecoder.newInstance(source.source().inputStream())
|
||||||
} else {
|
} 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]. */
|
/** Compute and set the scaling properties for [BitmapFactory.Options]. */
|
||||||
private fun BitmapFactory.Options.configureScale(srcWidth: Int, srcHeight: Int): Rect {
|
private fun BitmapFactory.Options.configureScale(srcWidth: Int, srcHeight: Int): Rect {
|
||||||
val dstWidth = options.size.widthPx(options.scale) { srcWidth }
|
val dstWidth = options.size.widthPx(options.scale) { srcWidth }
|
||||||
@@ -142,18 +106,41 @@ class RegionBitmapDecoder(
|
|||||||
return rect
|
return rect
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory(
|
private fun BitmapFactory.Options.configureConfig() {
|
||||||
maxParallelism: Int = DEFAULT_MAX_PARALLELISM,
|
var config = options.config
|
||||||
) : Decoder.Factory {
|
|
||||||
|
|
||||||
@Suppress("NEWER_VERSION_IN_SINCE_KOTLIN")
|
inMutable = false
|
||||||
@SinceKotlin("999.9") // Only public in Java.
|
|
||||||
constructor() : this()
|
|
||||||
|
|
||||||
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 {
|
// Decode the image as RGB_565 as an optimization if allowed.
|
||||||
return RegionBitmapDecoder(result.source, options, parallelismLock)
|
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
|
override fun equals(other: Any?) = other is Factory
|
||||||
@@ -164,22 +151,7 @@ class RegionBitmapDecoder(
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val PARAM_SCROLL = "scroll"
|
const val PARAM_SCROLL = "scroll"
|
||||||
|
const val PARAM_REGION = "region"
|
||||||
const val SCROLL_UNDEFINED = -1
|
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 coil.util.CoilUtils
|
||||||
import com.google.android.material.progressindicator.BaseProgressIndicator
|
import com.google.android.material.progressindicator.BaseProgressIndicator
|
||||||
import org.koitharu.kotatsu.R
|
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.AnimatedPlaceholderDrawable
|
||||||
import org.koitharu.kotatsu.core.ui.image.RegionBitmapDecoder
|
|
||||||
import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener
|
import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
@@ -63,7 +63,7 @@ fun ImageRequest.Builder.indicator(indicators: List<BaseProgressIndicator<*>>):
|
|||||||
|
|
||||||
fun ImageRequest.Builder.decodeRegion(
|
fun ImageRequest.Builder.decodeRegion(
|
||||||
scroll: Int = RegionBitmapDecoder.SCROLL_UNDEFINED,
|
scroll: Int = RegionBitmapDecoder.SCROLL_UNDEFINED,
|
||||||
): ImageRequest.Builder = decoderFactory(RegionBitmapDecoder.Factory())
|
): ImageRequest.Builder = setParameter(RegionBitmapDecoder.PARAM_REGION, true)
|
||||||
.setParameter(RegionBitmapDecoder.PARAM_SCROLL, scroll)
|
.setParameter(RegionBitmapDecoder.PARAM_SCROLL, scroll)
|
||||||
|
|
||||||
@Suppress("SpellCheckingInspection")
|
@Suppress("SpellCheckingInspection")
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ import android.os.Build
|
|||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.storage.StorageManager
|
import android.os.storage.StorageManager
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import org.jetbrains.annotations.Blocking
|
import org.jetbrains.annotations.Blocking
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.fs.FileSequence
|
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)
|
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 {
|
fun File.getStorageName(context: Context): String = runCatching {
|
||||||
val manager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
|
val manager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import okio.BufferedSink
|
|||||||
import okio.Source
|
import okio.Source
|
||||||
import org.koitharu.kotatsu.core.util.CancellableSource
|
import org.koitharu.kotatsu.core.util.CancellableSource
|
||||||
import org.koitharu.kotatsu.core.util.progress.ProgressResponseBody
|
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 {
|
fun ResponseBody.withProgress(progressState: MutableStateFlow<Float>): ResponseBody {
|
||||||
return ProgressResponseBody(this, progressState)
|
return ProgressResponseBody(this, progressState)
|
||||||
@@ -23,3 +26,10 @@ suspend fun Source.cancellable(): Source {
|
|||||||
suspend fun BufferedSink.writeAllCancellable(source: Source) = withContext(Dispatchers.IO) {
|
suspend fun BufferedSink.writeAllCancellable(source: Source) = withContext(Dispatchers.IO) {
|
||||||
writeAll(source.cancellable())
|
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
|
package org.koitharu.kotatsu.reader.domain
|
||||||
|
|
||||||
|
import android.content.ContentResolver.MimeTypeInfo
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.graphics.ImageDecoder
|
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.annotation.AnyThread
|
import androidx.annotation.AnyThread
|
||||||
import androidx.collection.LongSparseArray
|
import androidx.collection.LongSparseArray
|
||||||
import androidx.collection.set
|
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.mimeType
|
||||||
import org.koitharu.kotatsu.parsers.util.requireBody
|
import org.koitharu.kotatsu.parsers.util.requireBody
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
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 org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
@@ -144,8 +144,8 @@ class PageLoader @Inject constructor(
|
|||||||
ZipFile(uri.schemeSpecificPart).use { zip ->
|
ZipFile(uri.schemeSpecificPart).use { zip ->
|
||||||
val entry = zip.getEntry(uri.fragment)
|
val entry = zip.getEntry(uri.fragment)
|
||||||
context.ensureRamAtLeast(entry.size * 2)
|
context.ensureRamAtLeast(entry.size * 2)
|
||||||
zip.getInputStream(zip.getEntry(uri.fragment)).use {
|
zip.getInputStream(entry).use {
|
||||||
checkBitmapNotNull(BitmapFactory.decodeStream(it))
|
BitmapDecoderCompat.decode(it, entry.mimeType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,11 +154,7 @@ class PageLoader @Inject constructor(
|
|||||||
val file = uri.toFile()
|
val file = uri.toFile()
|
||||||
runInterruptible(Dispatchers.IO) {
|
runInterruptible(Dispatchers.IO) {
|
||||||
context.ensureRamAtLeast(file.length() * 2)
|
context.ensureRamAtLeast(file.length() * 2)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
BitmapDecoderCompat.decode(file)
|
||||||
ImageDecoder.decodeBitmap(ImageDecoder.createSource(file))
|
|
||||||
} else {
|
|
||||||
checkBitmapNotNull(BitmapFactory.decodeFile(file.absolutePath))
|
|
||||||
}
|
|
||||||
}.use { image ->
|
}.use { image ->
|
||||||
image.compressToPNG(file)
|
image.compressToPNG(file)
|
||||||
}
|
}
|
||||||
@@ -253,8 +249,6 @@ class PageLoader @Inject constructor(
|
|||||||
return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES)
|
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 {
|
private fun Deferred<Uri>.isValid(): Boolean {
|
||||||
return getCompletionResultOrNull()?.map { uri ->
|
return getCompletionResultOrNull()?.map { uri ->
|
||||||
uri.exists() && uri.isTargetNotEmpty()
|
uri.exists() && uri.isTargetNotEmpty()
|
||||||
|
|||||||
Reference in New Issue
Block a user