Drop legacy decoder (#3786)
This commit is contained in:
@@ -291,7 +291,6 @@ dependencies {
|
||||
}
|
||||
implementation(libs.image.decoder)
|
||||
|
||||
implementation(libs.image.decoder2)
|
||||
implementation(libs.webgpuviewer)
|
||||
implementation(libs.kim)
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package eu.kanade.domain.base
|
||||
|
||||
import android.content.Context
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -35,18 +34,6 @@ class BasePreferences(
|
||||
|
||||
val highQualityRenderer: Preference<Boolean> = preferenceStore.getBoolean("pref_high_quality_renderer_key", false)
|
||||
|
||||
val displayProfile: Preference<String> = preferenceStore.getString("pref_display_profile_key", "")
|
||||
|
||||
val hardwareBitmapThreshold: Preference<Int> = preferenceStore.getInt(
|
||||
"pref_hardware_bitmap_threshold",
|
||||
GLUtil.SAFE_TEXTURE_LIMIT,
|
||||
)
|
||||
|
||||
val alwaysDecodeLongStripWithSSIV: Preference<Boolean> = preferenceStore.getBoolean(
|
||||
"pref_always_decode_long_strip_with_ssiv",
|
||||
false,
|
||||
)
|
||||
|
||||
val installationId: Preference<String> = preferenceStore.getString(Preference.appStateKey("installation_id"), "")
|
||||
|
||||
val donationCampaignShown: Preference<Boolean> = preferenceStore.getBoolean(
|
||||
|
||||
@@ -53,7 +53,7 @@ import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.DropdownMenu
|
||||
import eu.kanade.presentation.manga.EditCoverAction
|
||||
import eu.kanade.tachiyomi.data.coil.ImageDecoder2
|
||||
import eu.kanade.tachiyomi.data.coil.ImageDecoder
|
||||
import eu.kanade.tachiyomi.data.coil.newDecoder
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -173,7 +173,7 @@ fun MangaCoverDialog(
|
||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||
.newDecoder(true)
|
||||
.target { result ->
|
||||
val res = (result as ImageDecoder2.DecodeResultImage).res
|
||||
val res = (result as ImageDecoder.DecodeResultImage).res
|
||||
val page = runBlocking(WebGpuRenderer.dispatcher) {
|
||||
ImagePage(res.image, res.width, res.height)
|
||||
}.apply {
|
||||
|
||||
-41
@@ -47,7 +47,6 @@ import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
|
||||
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
||||
import eu.kanade.tachiyomi.util.CrashLogUtil
|
||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
||||
import eu.kanade.tachiyomi.util.system.powerManager
|
||||
@@ -334,15 +333,6 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
basePreferences: BasePreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
val chooseColorProfile = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument(),
|
||||
) { uri ->
|
||||
uri?.let {
|
||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||
basePreferences.displayProfile.set(uri.toString())
|
||||
}
|
||||
}
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_reader),
|
||||
preferenceItems = listOf(
|
||||
@@ -350,37 +340,6 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
preference = basePreferences.highQualityRenderer,
|
||||
title = stringResource(MR.strings.pref_high_quality_renderer),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = basePreferences.hardwareBitmapThreshold,
|
||||
entries = GLUtil.CUSTOM_TEXTURE_LIMIT_OPTIONS
|
||||
.mapIndexed { index, option ->
|
||||
val display = if (index == 0) {
|
||||
stringResource(MR.strings.pref_hardware_bitmap_threshold_default, option)
|
||||
} else {
|
||||
option.toString()
|
||||
}
|
||||
option to display
|
||||
}
|
||||
.toMap(),
|
||||
title = stringResource(MR.strings.pref_hardware_bitmap_threshold),
|
||||
subtitleProvider = { value, options ->
|
||||
stringResource(MR.strings.pref_hardware_bitmap_threshold_summary, options[value].orEmpty())
|
||||
},
|
||||
enabled = !ImageUtil.HARDWARE_BITMAP_UNSUPPORTED &&
|
||||
GLUtil.DEVICE_TEXTURE_LIMIT > GLUtil.SAFE_TEXTURE_LIMIT,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = basePreferences.alwaysDecodeLongStripWithSSIV,
|
||||
title = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_2),
|
||||
subtitle = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_summary),
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_display_profile),
|
||||
subtitle = basePreferences.displayProfile.get(),
|
||||
onClick = {
|
||||
chooseColorProfile.launch(arrayOf("*/*"))
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,10 +30,10 @@ import eu.kanade.tachiyomi.core.security.PrivacyPreferences
|
||||
import eu.kanade.tachiyomi.crash.CrashActivity
|
||||
import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
|
||||
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
|
||||
import eu.kanade.tachiyomi.data.coil.ImageDecoder
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
|
||||
import eu.kanade.tachiyomi.data.coil.MangaKeyer
|
||||
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.di.AppModule
|
||||
import eu.kanade.tachiyomi.di.PreferenceModule
|
||||
@@ -41,7 +41,6 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
@@ -144,14 +143,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
.onEach(TelemetryConfig::setCrashlyticsEnabled)
|
||||
.launchIn(scope)
|
||||
|
||||
basePreferences.hardwareBitmapThreshold.let { preference ->
|
||||
if (!preference.isSet()) preference.set(GLUtil.DEVICE_TEXTURE_LIMIT)
|
||||
}
|
||||
|
||||
basePreferences.hardwareBitmapThreshold.changes()
|
||||
.onEach { ImageUtil.hardwareBitmapThreshold = it }
|
||||
.launchIn(scope)
|
||||
|
||||
setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode.get())
|
||||
|
||||
// Updates widget update
|
||||
@@ -192,7 +183,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
// NetworkFetcher.Factory
|
||||
add(OkHttpNetworkFetcherFactory(callFactoryLazy::value))
|
||||
// Decoder.Factory
|
||||
add(TachiyomiImageDecoder.Factory())
|
||||
add(ImageDecoder.Factory())
|
||||
// Fetcher.Factory
|
||||
add(BufferedSourceFetcher.Factory())
|
||||
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy))
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.graphics.scale
|
||||
import ca.mpreg.imagedecoder.ImageDecoder
|
||||
import coil3.Canvas
|
||||
import coil3.Image
|
||||
import coil3.ImageLoader
|
||||
import coil3.asImage
|
||||
import coil3.decode.DecodeResult
|
||||
import coil3.decode.DecodeUtils
|
||||
import coil3.decode.Decoder
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.request.Options
|
||||
import logcat.LogPriority
|
||||
import okio.BufferedSource
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
|
||||
/**
|
||||
* A [Decoder] that uses [ImageDecoder] (libvips-based) to decode image formats not supported
|
||||
* by the Android system decoder (AVIF, JXL, HEIF, etc.).
|
||||
*/
|
||||
class ImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder {
|
||||
|
||||
/**
|
||||
* Wraps a raw [ImageDecoder.DecodeResult] as a Coil [Image] for callers that want
|
||||
* direct access to the RGBA [java.nio.ByteBuffer] (e.g. the new-decoder path).
|
||||
*/
|
||||
class DecodeResultImage(val res: ImageDecoder.DecodeResult) : Image {
|
||||
override val size: Long get() = res.image.capacity().toLong()
|
||||
override val width: Int get() = res.width
|
||||
override val height: Int get() = res.height
|
||||
override val shareable: Boolean get() = true
|
||||
override fun draw(canvas: Canvas) {}
|
||||
}
|
||||
|
||||
override suspend fun decode(): DecodeResult {
|
||||
val decoder = resources.sourceOrNull()?.use {
|
||||
try {
|
||||
ImageDecoder.new(it.inputStream())
|
||||
} catch (e: ImageDecoder.DecodeException) {
|
||||
logcat(LogPriority.ERROR, e) { "ImageDecoder.new failed: ${e.message}" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
check(decoder != null && decoder.pages > 0) { "Failed to initialize decoder" }
|
||||
|
||||
val res = decoder.decode()
|
||||
|
||||
val srcWidth = res.width
|
||||
val srcHeight = res.height
|
||||
|
||||
// newDecoder path: caller wants the raw DecodeResult (e.g. for custom rendering).
|
||||
// Hand it back as-is; sampling is the caller's responsibility.
|
||||
if (options.newDecoder) {
|
||||
return DecodeResult(
|
||||
image = DecodeResultImage(res),
|
||||
isSampled = false,
|
||||
)
|
||||
}
|
||||
|
||||
// Normal path: produce a Bitmap scaled to the requested output size.
|
||||
val dstWidth = options.size.widthPx(options.scale) { srcWidth }
|
||||
val dstHeight = options.size.heightPx(options.scale) { srcHeight }
|
||||
val sampleSize = DecodeUtils.calculateInSampleSize(
|
||||
srcWidth = srcWidth,
|
||||
srcHeight = srcHeight,
|
||||
dstWidth = dstWidth,
|
||||
dstHeight = dstHeight,
|
||||
scale = options.scale,
|
||||
)
|
||||
|
||||
// Copy RGBA pixels from the native buffer into a full-resolution bitmap.
|
||||
// We must do this while `res` (and its native memory) is still alive.
|
||||
val fullBitmap = createBitmap(srcWidth, srcHeight)
|
||||
res.image.rewind()
|
||||
fullBitmap.copyPixelsFromBuffer(res.image)
|
||||
|
||||
// Downsample if needed. sampleSize is a power-of-two factor; the target
|
||||
// dimensions are src / sampleSize, matching BitmapFactory inSampleSize behaviour.
|
||||
val bitmap = if (sampleSize > 1) {
|
||||
val scaledWidth = (srcWidth / sampleSize).coerceAtLeast(1)
|
||||
val scaledHeight = (srcHeight / sampleSize).coerceAtLeast(1)
|
||||
val scaled = fullBitmap.scale(scaledWidth, scaledHeight)
|
||||
fullBitmap.recycle()
|
||||
scaled
|
||||
} else {
|
||||
fullBitmap
|
||||
}
|
||||
|
||||
return DecodeResult(
|
||||
image = bitmap.asImage(),
|
||||
isSampled = sampleSize > 1,
|
||||
)
|
||||
}
|
||||
|
||||
class Factory : Decoder.Factory {
|
||||
override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
|
||||
return if (options.newDecoder || options.customDecoder || isApplicable(result.source.source())) {
|
||||
ImageDecoder(result.source, options)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isApplicable(source: BufferedSource): Boolean {
|
||||
val type = source.peek().inputStream().use {
|
||||
ImageUtil.findImageType(it)
|
||||
}
|
||||
return when (type) {
|
||||
ImageUtil.ImageType.AVIF,
|
||||
ImageUtil.ImageType.JXL,
|
||||
ImageUtil.ImageType.HEIF,
|
||||
ImageUtil.ImageType.JP2,
|
||||
-> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?) = other is Factory
|
||||
|
||||
override fun hashCode() = javaClass.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import coil3.Canvas
|
||||
import coil3.Image
|
||||
import coil3.ImageLoader
|
||||
import coil3.asImage
|
||||
import coil3.decode.DecodeResult
|
||||
import coil3.decode.DecodeUtils
|
||||
import coil3.decode.Decoder
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.request.Options
|
||||
import coil3.request.bitmapConfig
|
||||
import okio.BufferedSource
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import tachiyomi.decoder.ImageDecoder
|
||||
|
||||
/**
|
||||
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
|
||||
*/
|
||||
class TachiyomiImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder {
|
||||
|
||||
override suspend fun decode(): DecodeResult {
|
||||
val decoder = resources.sourceOrNull()?.use {
|
||||
ImageDecoder.newInstance(it.inputStream(), options.cropBorders, displayProfile)
|
||||
}
|
||||
|
||||
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder" }
|
||||
|
||||
val srcWidth = decoder.width
|
||||
val srcHeight = decoder.height
|
||||
|
||||
val dstWidth = options.size.widthPx(options.scale) { srcWidth }
|
||||
val dstHeight = options.size.heightPx(options.scale) { srcHeight }
|
||||
|
||||
val sampleSize = DecodeUtils.calculateInSampleSize(
|
||||
srcWidth = srcWidth,
|
||||
srcHeight = srcHeight,
|
||||
dstWidth = dstWidth,
|
||||
dstHeight = dstHeight,
|
||||
scale = options.scale,
|
||||
)
|
||||
|
||||
var bitmap = decoder.decode(sampleSize = sampleSize)
|
||||
decoder.recycle()
|
||||
|
||||
check(bitmap != null) { "Failed to decode image" }
|
||||
|
||||
if (options.bitmapConfig == Bitmap.Config.HARDWARE && ImageUtil.canUseHardwareBitmap(bitmap)) {
|
||||
val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false)
|
||||
if (hwBitmap != null) {
|
||||
bitmap.recycle()
|
||||
bitmap = hwBitmap
|
||||
}
|
||||
}
|
||||
|
||||
return DecodeResult(
|
||||
image = bitmap.asImage(),
|
||||
isSampled = sampleSize > 1,
|
||||
)
|
||||
}
|
||||
|
||||
class Factory : Decoder.Factory {
|
||||
|
||||
override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
|
||||
return if (options.newDecoder) {
|
||||
ImageDecoder2(result.source, options)
|
||||
} else if (options.customDecoder || isApplicable(result.source.source())) {
|
||||
TachiyomiImageDecoder(result.source, options)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isApplicable(source: BufferedSource): Boolean {
|
||||
val type = source.peek().inputStream().use {
|
||||
ImageUtil.findImageType(it)
|
||||
}
|
||||
return when (type) {
|
||||
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL, ImageUtil.ImageType.HEIF -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?) = other is Factory
|
||||
|
||||
override fun hashCode() = javaClass.hashCode()
|
||||
}
|
||||
|
||||
companion object {
|
||||
var displayProfile: ByteArray? = null
|
||||
}
|
||||
}
|
||||
|
||||
class ImageDecoder2(private val resources: ImageSource, private val options: Options) : Decoder {
|
||||
|
||||
class DecodeResultImage(val res: ca.mpreg.imagedecoder.ImageDecoder.DecodeResult) : Image {
|
||||
override val size: Long get() = res.image.capacity().toLong()
|
||||
override val width: Int get() = res.width
|
||||
override val height: Int get() = res.height
|
||||
override val shareable: Boolean get() = true
|
||||
override fun draw(canvas: Canvas) {}
|
||||
}
|
||||
|
||||
override suspend fun decode(): DecodeResult {
|
||||
val source = resources.source()
|
||||
|
||||
val decoder = ca.mpreg.imagedecoder.ImageDecoder.new(source.inputStream())
|
||||
|
||||
val res = decoder.decode()
|
||||
|
||||
return DecodeResult(
|
||||
image = DecodeResultImage(res),
|
||||
isSampled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,6 @@ import eu.kanade.presentation.reader.appbars.ReaderAppBars
|
||||
import eu.kanade.presentation.reader.components.ChapterNavigatorType
|
||||
import eu.kanade.presentation.reader.settings.ReaderSettingsDialog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
|
||||
@@ -868,10 +867,6 @@ class ReaderActivity : BaseActivity() {
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
preferences.displayProfile.changes()
|
||||
.onEach { setDisplayProfile(it) }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
readerPreferences.keepScreenOn.changes()
|
||||
.onEach(::setKeepScreenOn)
|
||||
.launchIn(lifecycleScope)
|
||||
@@ -899,25 +894,6 @@ class ReaderActivity : BaseActivity() {
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the display profile to [path].
|
||||
*/
|
||||
private fun setDisplayProfile(path: String) {
|
||||
val file = UniFile.fromUri(baseContext, path.toUri())
|
||||
if (file != null && file.exists()) {
|
||||
val inputStream = file.openInputStream()
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
inputStream.use { input ->
|
||||
outputStream.use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
val data = outputStream.toByteArray()
|
||||
SubsamplingScaleImageView.setDisplayProfile(data)
|
||||
TachiyomiImageDecoder.displayProfile = data
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the keep screen on mode according to [enabled].
|
||||
*/
|
||||
|
||||
@@ -60,10 +60,6 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
private val isWebtoon: Boolean = false,
|
||||
) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) {
|
||||
|
||||
private val alwaysDecodeLongStripWithSSIV by lazy {
|
||||
Injekt.get<BasePreferences>().alwaysDecodeLongStripWithSSIV.get()
|
||||
}
|
||||
|
||||
private var pageView: View? = null
|
||||
|
||||
private var config: Config? = null
|
||||
@@ -240,7 +236,6 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
} else {
|
||||
SubsamplingScaleImageView(context)
|
||||
}.apply {
|
||||
setMaxTileSize(ImageUtil.hardwareBitmapThreshold)
|
||||
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER)
|
||||
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
|
||||
setMinimumTileDpi(180)
|
||||
@@ -301,8 +296,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
isVisible = true
|
||||
}
|
||||
is BufferedSource -> {
|
||||
if (!isWebtoon || alwaysDecodeLongStripWithSSIV) {
|
||||
setHardwareConfig(ImageUtil.canUseHardwareBitmap(data))
|
||||
if (!isWebtoon) {
|
||||
setImage(ImageSource.inputStream(data.inputStream()))
|
||||
isVisible = true
|
||||
return@apply
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PointF
|
||||
import android.util.Log
|
||||
import android.view.InputDevice
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
@@ -52,6 +51,8 @@ import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
@@ -170,14 +171,14 @@ open class WebGpuViewer(
|
||||
is TransitionPage -> createTransitionPage(page)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("WebGpuViewer", "Decode error: ${pageKey(page)}", e)
|
||||
logcat(LogPriority.ERROR, e) { "Decode error: ${pageKey(page)}" }
|
||||
synchronized(lock) { if (pageInCache(page)) page.state = PageState.IDLE }
|
||||
}
|
||||
}
|
||||
} catch (_: InterruptedException) {
|
||||
// Normal shutdown
|
||||
} catch (e: Exception) {
|
||||
Log.e("WebGpuViewer", "Decode worker died", e)
|
||||
logcat(LogPriority.ERROR, e) { "Decode worker died" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -688,7 +689,7 @@ open class WebGpuViewer(
|
||||
when (state) {
|
||||
Page.State.Queue, Page.State.LoadPage, Page.State.DownloadImage -> true
|
||||
is Page.State.Error -> {
|
||||
Log.e("WebGpuViewer", "Page load error: ${state.error}")
|
||||
logcat(LogPriority.ERROR) { "Page load error: ${state.error}" }
|
||||
false
|
||||
}
|
||||
|
||||
@@ -711,7 +712,7 @@ open class WebGpuViewer(
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("WebGpuViewer", "startPageLoad error", e)
|
||||
logcat(LogPriority.ERROR, e) { "startPageLoad error" }
|
||||
synchronized(lock) { if (pageInCache(page)) page.state = PageState.IDLE }
|
||||
}
|
||||
}
|
||||
@@ -762,11 +763,60 @@ open class WebGpuViewer(
|
||||
Image.Position.SINGLE
|
||||
}
|
||||
|
||||
val dec = ImageDecoder.new(bytes?.inputStream() ?: input)
|
||||
val dec = try {
|
||||
ImageDecoder.new(bytes?.inputStream() ?: input)
|
||||
} catch (e: ImageDecoder.DecodeException) {
|
||||
logcat(LogPriority.ERROR, e) { "ImageDecoder.new failed: ${e.message}" }
|
||||
val errorMessage = e.message ?: "Failed to decode image"
|
||||
val bitmap = createBitmap(pager.state.width.coerceAtLeast(1), pager.state.height.coerceAtLeast(1))
|
||||
val canvas = Canvas(bitmap)
|
||||
canvas.drawColor(readerBackgroundColor())
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = readerOnBackgroundColor()
|
||||
textSize = 36f
|
||||
textAlign = Paint.Align.CENTER
|
||||
}
|
||||
val maxWidth = bitmap.width * 0.8f
|
||||
val words = errorMessage.split(" ")
|
||||
val lines = mutableListOf<String>()
|
||||
var currentLine = StringBuilder()
|
||||
for (word in words) {
|
||||
val testLine = if (currentLine.isEmpty()) word else "$currentLine $word"
|
||||
if (paint.measureText(testLine) <= maxWidth) {
|
||||
currentLine = StringBuilder(testLine)
|
||||
} else {
|
||||
if (currentLine.isNotEmpty()) lines.add(currentLine.toString())
|
||||
currentLine = StringBuilder(word)
|
||||
}
|
||||
}
|
||||
if (currentLine.isNotEmpty()) lines.add(currentLine.toString())
|
||||
val lineHeight = 40f
|
||||
var y = bitmap.height / 2f - lines.size * lineHeight / 2
|
||||
for (line in lines) {
|
||||
canvas.drawText(line, bitmap.width / 2f, y, paint)
|
||||
y += lineHeight
|
||||
}
|
||||
val errorPage = ImagePage(bitmap, createMipMaps = false).also {
|
||||
it.image?.position = Image.Position.SINGLE
|
||||
}
|
||||
synchronized(lock) {
|
||||
if (pageInCache(page) && !page.imagePage.isDecoded && !page.imagePage.destroyed) {
|
||||
val oldImagePage = page.imagePage
|
||||
page.imagePage = errorPage
|
||||
page.state = PageState.IDLE
|
||||
if (oldImagePage !is ImagePage.Dummy) oldImagePage.cleanup()
|
||||
pager.state.invalidate()
|
||||
} else {
|
||||
if (pageInCache(page)) page.state = PageState.IDLE
|
||||
errorPage.cleanup()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
val pageCount = dec.pages
|
||||
|
||||
if (pageCount == 0) {
|
||||
Log.e("WebGpuViewer", "decodeReaderPage: no frames decoded")
|
||||
logcat(LogPriority.ERROR) { "decodeReaderPage: no frames decoded" }
|
||||
synchronized(lock) { if (pageInCache(page)) page.state = PageState.IDLE }
|
||||
return
|
||||
}
|
||||
@@ -838,7 +888,7 @@ open class WebGpuViewer(
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("WebGpuViewer", "decodeReaderPage error", e)
|
||||
logcat(LogPriority.ERROR, e) { "decodeReaderPage error" }
|
||||
synchronized(lock) { if (pageInCache(page)) page.state = PageState.IDLE }
|
||||
} finally {
|
||||
imagePage?.cleanup()
|
||||
@@ -921,7 +971,7 @@ open class WebGpuViewer(
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("WebGpuViewer", "createTransitionPage error", e)
|
||||
logcat(LogPriority.ERROR, e) { "createTransitionPage error" }
|
||||
synchronized(lock) { if (pageInCache(page)) page.state = PageState.IDLE }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user