Bump webgpuviewer (#3949)
* Continuous background color * Use ImageDecoder instead of Kim * Only get tag when in dual mode * Update libs.versions.toml * Make progress circle smaller and animate progress * Fix long strip showing the menu at ends
This commit is contained in:
@@ -298,7 +298,6 @@ dependencies {
|
|||||||
implementation(libs.image.decoder)
|
implementation(libs.image.decoder)
|
||||||
|
|
||||||
implementation(libs.webgpuviewer)
|
implementation(libs.webgpuviewer)
|
||||||
implementation(libs.kim)
|
|
||||||
|
|
||||||
// UI libraries
|
// UI libraries
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
|
|||||||
@@ -13,10 +13,8 @@ import coil3.decode.Decoder
|
|||||||
import coil3.decode.ImageSource
|
import coil3.decode.ImageSource
|
||||||
import coil3.fetch.SourceFetchResult
|
import coil3.fetch.SourceFetchResult
|
||||||
import coil3.request.Options
|
import coil3.request.Options
|
||||||
import logcat.LogPriority
|
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
import tachiyomi.core.common.util.system.ImageUtil
|
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
|
* A [Decoder] that uses [ImageDecoder] (libvips-based) to decode image formats not supported
|
||||||
@@ -37,18 +35,9 @@ class ImageDecoder(private val resources: ImageSource, private val options: Opti
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun decode(): DecodeResult {
|
override suspend fun decode(): DecodeResult {
|
||||||
val decoder = resources.source().use {
|
val res = resources.source().use {
|
||||||
try {
|
ImageDecoder.new(it.inputStream()).use { dec -> dec.decode() }
|
||||||
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 srcWidth = res.width
|
||||||
val srcHeight = res.height
|
val srcHeight = res.height
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import android.view.InputDevice
|
|||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.animate
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastCoerceIn
|
import androidx.compose.ui.util.fastCoerceIn
|
||||||
@@ -33,9 +36,6 @@ import ca.mpreg.webgpuviewer.transition.TransitionStackUp
|
|||||||
import ca.mpreg.webgpuviewer.viewer.ImagePage
|
import ca.mpreg.webgpuviewer.viewer.ImagePage
|
||||||
import ca.mpreg.webgpuviewer.viewer.ImageViewerContinuousState
|
import ca.mpreg.webgpuviewer.viewer.ImageViewerContinuousState
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
import de.stefan_oltmann.kim.Kim
|
|
||||||
import de.stefan_oltmann.kim.android.readMetadata
|
|
||||||
import de.stefan_oltmann.kim.format.tiff.constant.TiffTag
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
@@ -50,6 +50,7 @@ import eu.kanade.tachiyomi.util.system.createReaderThemeContext
|
|||||||
import eu.kanade.tachiyomi.util.system.readerBackgroundColor
|
import eu.kanade.tachiyomi.util.system.readerBackgroundColor
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
@@ -83,7 +84,7 @@ open class WebGpuViewer(
|
|||||||
@Volatile
|
@Volatile
|
||||||
private var cachedOnBackgroundColor: Int? = null
|
private var cachedOnBackgroundColor: Int? = null
|
||||||
|
|
||||||
private fun readerBackgroundColor(): Int =
|
protected fun readerBackgroundColor(): Int =
|
||||||
cachedBackgroundColor ?: activity.baseContext.readerBackgroundColor(config.theme)
|
cachedBackgroundColor ?: activity.baseContext.readerBackgroundColor(config.theme)
|
||||||
.also { cachedBackgroundColor = it }
|
.also { cachedBackgroundColor = it }
|
||||||
|
|
||||||
@@ -93,7 +94,7 @@ open class WebGpuViewer(
|
|||||||
Color.WHITE,
|
Color.WHITE,
|
||||||
).also { cachedOnBackgroundColor = it }
|
).also { cachedOnBackgroundColor = it }
|
||||||
|
|
||||||
private val scope = MainScope()
|
protected val scope = MainScope()
|
||||||
|
|
||||||
// Dedicated thread for decode worker to avoid blocking Dispatchers.Default pool
|
// Dedicated thread for decode worker to avoid blocking Dispatchers.Default pool
|
||||||
private val decodeExecutor = Executors.newSingleThreadExecutor { r ->
|
private val decodeExecutor = Executors.newSingleThreadExecutor { r ->
|
||||||
@@ -147,6 +148,10 @@ open class WebGpuViewer(
|
|||||||
|
|
||||||
private fun findInCache(key: PageKey): ViewerPage? = pageCache[key]
|
private fun findInCache(key: PageKey): ViewerPage? = pageCache[key]
|
||||||
|
|
||||||
|
protected fun viewerPageFor(imagePage: ImagePage): ViewerPage? = synchronized(lock) {
|
||||||
|
pageCache.values.firstOrNull { it.imagePage === imagePage }
|
||||||
|
}
|
||||||
|
|
||||||
/** Check if a page is in the cache by identity. O(1) via key lookup. */
|
/** Check if a page is in the cache by identity. O(1) via key lookup. */
|
||||||
private fun pageInCache(page: ViewerPage): Boolean = pageCache[pageKey(page)] === page
|
private fun pageInCache(page: ViewerPage): Boolean = pageCache[pageKey(page)] === page
|
||||||
|
|
||||||
@@ -513,10 +518,45 @@ open class WebGpuViewer(
|
|||||||
homeScale = 1f
|
homeScale = 1f
|
||||||
}
|
}
|
||||||
|
|
||||||
var progress: Float = 0f
|
@Volatile
|
||||||
|
private var progressValue: Float = 0f
|
||||||
|
private var progressJob: Job? = null
|
||||||
|
var progress: Float
|
||||||
|
get() = progressValue
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
val target = value.fastCoerceIn(0f, 1f)
|
||||||
|
synchronized(this) {
|
||||||
|
progressJob?.cancel()
|
||||||
|
progressJob = null
|
||||||
|
|
||||||
|
if (destroyed || progressValue == target) return
|
||||||
|
|
||||||
|
val scope = scope ?: run {
|
||||||
|
progressValue = target
|
||||||
invalidate()
|
invalidate()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val start = progressValue
|
||||||
|
progressJob = scope.launch {
|
||||||
|
animate(
|
||||||
|
start,
|
||||||
|
target,
|
||||||
|
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
|
||||||
|
) { current, _ ->
|
||||||
|
progressValue = current
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cleanup() {
|
||||||
|
super.cleanup()
|
||||||
|
synchronized(this) {
|
||||||
|
progressJob?.cancel()
|
||||||
|
progressJob = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var foregroundColor: Int = foregroundColor
|
var foregroundColor: Int = foregroundColor
|
||||||
@@ -534,9 +574,7 @@ open class WebGpuViewer(
|
|||||||
val cx = dst.width * (0.5f + scale * x)
|
val cx = dst.width * (0.5f + scale * x)
|
||||||
val cy = dst.height * (0.5f + scale * y)
|
val cy = dst.height * (0.5f + scale * y)
|
||||||
|
|
||||||
// Off this page's own width, not dst's: a spread half would otherwise draw a ring
|
val full = min(width, height) * 0.25f * scale
|
||||||
// sized for the whole screen, straight over its partner.
|
|
||||||
val full = width * 0.5f * scale
|
|
||||||
|
|
||||||
circle(cx, cy, full / 2f, 0xAAAAAAAA.toInt())
|
circle(cx, cy, full / 2f, 0xAAAAAAAA.toInt())
|
||||||
|
|
||||||
@@ -925,6 +963,7 @@ open class WebGpuViewer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
(this as? ImageViewerContinuousState)?.let {
|
(this as? ImageViewerContinuousState)?.let {
|
||||||
|
backgroundColor = readerBackgroundColor()
|
||||||
homeScale = config.continuousMinWidth / 100f
|
homeScale = config.continuousMinWidth / 100f
|
||||||
scale = homeScale
|
scale = homeScale
|
||||||
minScale = if (config.zoomOutDisabled) 0f else 0.1f
|
minScale = if (config.zoomOutDisabled) 0f else 0.1f
|
||||||
@@ -1089,27 +1128,6 @@ open class WebGpuViewer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffered to read the spread tag, then decoded from the buffer. On the preference,
|
|
||||||
// not isDualPageMode(): WIDE is portrait-off, and a rotate never re-decodes. Never in
|
|
||||||
// continuous, where nothing pairs - that mode reads the stream instead of holding it.
|
|
||||||
val bytes = if (!isContinuous && config.dualPageView != ReaderPreferences.DualPageView.NEVER) {
|
|
||||||
input.readBytes()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Left untouched for a file that names no side - [spreadPosition] then derives one.
|
|
||||||
if (bytes != null) {
|
|
||||||
val tag = Kim.readMetadata(bytes.inputStream(), bytes.size.toLong())
|
|
||||||
?.findStringValue(TiffTag.TIFF_TAG_PAGE_NAME)
|
|
||||||
page.taggedSpreadPosition = when (tag) {
|
|
||||||
"Left" -> SpreadPosition.LEFT
|
|
||||||
"Right" -> SpreadPosition.RIGHT
|
|
||||||
null -> null
|
|
||||||
else -> SpreadPosition.SINGLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The decoder hands the map over unapplied - see ImageDecoder.Gainmap - because how
|
// The decoder hands the map over unapplied - see ImageDecoder.Gainmap - because how
|
||||||
// much of it to use depends on the display, so the viewer applies it.
|
// much of it to use depends on the display, so the viewer applies it.
|
||||||
fun ImageDecoder.DecodeResult.gainmapInput(): GainmapInput? = gainmap?.let {
|
fun ImageDecoder.DecodeResult.gainmapInput(): GainmapInput? = gainmap?.let {
|
||||||
@@ -1126,7 +1144,15 @@ open class WebGpuViewer(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val dec = ImageDecoder.new(bytes?.inputStream() ?: input)
|
ImageDecoder.new(input).use { dec ->
|
||||||
|
if (isDualPageMode()) {
|
||||||
|
page.taggedSpreadPosition = when (dec.getTag("PageName")) {
|
||||||
|
"Left" -> SpreadPosition.LEFT
|
||||||
|
"Right" -> SpreadPosition.RIGHT
|
||||||
|
null -> null
|
||||||
|
else -> SpreadPosition.SINGLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val pageCount = dec.pages
|
val pageCount = dec.pages
|
||||||
|
|
||||||
@@ -1245,6 +1271,7 @@ open class WebGpuViewer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun applyWideZoomIfNeeded(page: ImagePage.ImageSingle): Boolean {
|
private fun applyWideZoomIfNeeded(page: ImagePage.ImageSingle): Boolean {
|
||||||
if (!config.landscapeZoom) return false
|
if (!config.landscapeZoom) return false
|
||||||
@@ -1405,10 +1432,13 @@ open class WebGpuViewer(
|
|||||||
// MainScope, not the state's: that one dispatches inside the frame callback.
|
// MainScope, not the state's: that one dispatches inside the frame callback.
|
||||||
val settled = page
|
val settled = page
|
||||||
this@WebGpuViewer.scope.launch {
|
this@WebGpuViewer.scope.launch {
|
||||||
|
if (!isContinuous) {
|
||||||
activity.hideMenu()
|
activity.hideMenu()
|
||||||
progressPage(settled)?.let { activity.onPageSelected(it.page) }
|
progressPage(settled)?.let { activity.onPageSelected(it.page) }
|
||||||
|
}
|
||||||
preloadPages(settled)
|
preloadPages(settled)
|
||||||
|
|
||||||
|
if (!isContinuous) {
|
||||||
(settled as? ViewerTransitionPage)?.let { transitionPage ->
|
(settled as? ViewerTransitionPage)?.let { transitionPage ->
|
||||||
if (transitionPage.prevChapter == null || transitionPage.nextChapter == null) {
|
if (transitionPage.prevChapter == null || transitionPage.nextChapter == null) {
|
||||||
activity.showMenu()
|
activity.showMenu()
|
||||||
@@ -1416,6 +1446,7 @@ open class WebGpuViewer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-13
@@ -2,9 +2,9 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webgpu
|
|||||||
|
|
||||||
import ca.mpreg.webgpuviewer.ImageViewContinuous
|
import ca.mpreg.webgpuviewer.ImageViewContinuous
|
||||||
import ca.mpreg.webgpuviewer.viewer.ImagePage
|
import ca.mpreg.webgpuviewer.viewer.ImagePage
|
||||||
import ca.mpreg.webgpuviewer.viewer.ImageViewerContinuousState
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class WebGpuViewerContinuous(activity: ReaderActivity, val useGap: Boolean = false) :
|
class WebGpuViewerContinuous(activity: ReaderActivity, val useGap: Boolean = false) :
|
||||||
@@ -15,24 +15,69 @@ class WebGpuViewerContinuous(activity: ReaderActivity, val useGap: Boolean = fal
|
|||||||
// How many pages the viewport shows depends on the zoom, and a page on screen has to be
|
// How many pages the viewport shows depends on the zoom, and a page on screen has to be
|
||||||
// decoded rather than merely reserved - so the window follows what the last frame reached.
|
// decoded rather than merely reserved - so the window follows what the last frame reached.
|
||||||
override val preloadAhead get() = max(3, state.pagesBelow)
|
override val preloadAhead get() = max(3, state.pagesBelow)
|
||||||
override val preloadBehind get() = max(1, state.pagesAbove)
|
override val preloadBehind get() = max(2, state.pagesAbove)
|
||||||
|
|
||||||
// The state reaches MAX_VISIBLE_PAGES either side of the current page whatever the zoom - to
|
override val cacheSize get() = 2 + 2 * max(4, state.pagesAbove + state.pagesBelow)
|
||||||
// measure the document's end as well as to draw - and every page in that reach is created on
|
|
||||||
// demand here. Sized under it, each frame evicts exactly what the next one asks for.
|
|
||||||
override val cacheSize get() = 2 + 2 * ImageViewerContinuousState.MAX_VISIBLE_PAGES
|
|
||||||
|
|
||||||
private val state get() = (pager as ImageViewContinuous).state
|
private val state get() = (pager as ImageViewContinuous).state
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Scrolling clear of a transition page is the only point this mode can call the chapter
|
state.backgroundColor = readerBackgroundColor()
|
||||||
// before it finished - reaching a page's top comes a screen too early. Reported on every
|
|
||||||
// change, so scrolling back up over it and down again selects that last page again.
|
state.onViewport = { readThrough ->
|
||||||
state.onPageScrolledThrough = onScrolledThrough@{ imagePage ->
|
val pageChanged = readThrough !== lastReadThrough
|
||||||
val chapter = (imagePage as? TransitionPage)?.prevChapter ?: return@onScrolledThrough
|
if (pageChanged) {
|
||||||
val lastPage = chapter.pages?.lastOrNull() ?: return@onScrolledThrough
|
lastReadThrough = readThrough
|
||||||
activity.onPageSelected(lastPage)
|
readThrough?.let { select(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val page0 = state.getPage(0)
|
||||||
|
val edge = (page0 is TransitionPage && page0.prevChapter == null) ||
|
||||||
|
(readThrough is TransitionPage && readThrough.nextChapter == null)
|
||||||
|
|
||||||
|
val first = wasAtEdge == null
|
||||||
|
val edgeChanged = edge != wasAtEdge
|
||||||
|
wasAtEdge = edge
|
||||||
|
if (edge) {
|
||||||
|
if (edgeChanged) scope.launch { activity.showMenu() }
|
||||||
|
} else if (!first && (edgeChanged || pageChanged)) {
|
||||||
|
scope.launch { activity.hideMenu() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Null until the first frame, so opening mid-document neither shows nor hides. */
|
||||||
|
@Volatile
|
||||||
|
private var wasAtEdge: Boolean? = null
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var lastReadThrough: ImagePage? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark [imagePage] read - its bottom has cleared the viewport, which is later than
|
||||||
|
* onPageChange's "top arrived" and the reason this mode selects from here: scrolling up to
|
||||||
|
* reveal the page above would otherwise walk progress back past pages still on screen.
|
||||||
|
*/
|
||||||
|
private fun select(imagePage: ImagePage) {
|
||||||
|
val page = readerPageFor(imagePage) ?: return
|
||||||
|
scope.launch { activity.onPageSelected(page) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readerPageFor(imagePage: ImagePage): ReaderPage? {
|
||||||
|
// Off the page itself: a transition page is never currentPage (the scroll walk stops
|
||||||
|
// before it), so it is the first thing a preload evicts.
|
||||||
|
if (imagePage is TransitionPage) return imagePage.prevChapter?.pages?.lastOrNull()
|
||||||
|
|
||||||
|
// Only an image page needs the lookup - nothing on it names its ReaderPage. Evicted
|
||||||
|
// means scrolled well clear of, so there is no progress left to report.
|
||||||
|
return (viewerPageFor(imagePage) as? ViewerReaderPage)?.page
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun destroy() {
|
||||||
|
state.onViewport = null
|
||||||
|
lastReadThrough = null
|
||||||
|
wasAtEdge = null
|
||||||
|
super.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scrollByHalfPage(direction: Int) {
|
private fun scrollByHalfPage(direction: Int) {
|
||||||
@@ -45,6 +90,8 @@ class WebGpuViewerContinuous(activity: ReaderActivity, val useGap: Boolean = fal
|
|||||||
|
|
||||||
override fun moveToPage(page: ReaderPage) {
|
override fun moveToPage(page: ReaderPage) {
|
||||||
super.moveToPage(page)
|
super.moveToPage(page)
|
||||||
|
wasAtEdge = null
|
||||||
|
lastReadThrough = null
|
||||||
// Also for a jump to the page already showing, which turns nothing to slide in.
|
// Also for a jump to the page already showing, which turns nothing to slide in.
|
||||||
state.resetScroll()
|
state.resetScroll()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ object ImageUtil {
|
|||||||
|
|
||||||
fun findImageType(stream: InputStream): ImageType? {
|
fun findImageType(stream: InputStream): ImageType? {
|
||||||
return try {
|
return try {
|
||||||
val decoder = ImageDecoder.new(stream)
|
val format = ImageDecoder.new(stream).use { dec -> dec.format }
|
||||||
when (decoder.format) {
|
when (format) {
|
||||||
"jpeg" -> ImageType.JPEG
|
"jpeg" -> ImageType.JPEG
|
||||||
"png" -> ImageType.PNG
|
"png" -> ImageType.PNG
|
||||||
"webp" -> ImageType.WEBP
|
"webp" -> ImageType.WEBP
|
||||||
@@ -76,8 +76,7 @@ object ImageUtil {
|
|||||||
ImageType.GIF -> true
|
ImageType.GIF -> true
|
||||||
ImageType.WEBP, ImageType.HEIF -> {
|
ImageType.WEBP, ImageType.HEIF -> {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return false
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return false
|
||||||
val decoder = ImageDecoder.new(source.peek().inputStream())
|
ImageDecoder.new(source.peek().inputStream()).use { dec -> dec.pages > 1 }
|
||||||
decoder.pages > 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
@@ -303,19 +302,17 @@ object ImageUtil {
|
|||||||
* Algorithm for determining what background to accompany a comic/manga page
|
* Algorithm for determining what background to accompany a comic/manga page
|
||||||
*/
|
*/
|
||||||
fun chooseBackground(context: Context, imageStream: InputStream): Drawable {
|
fun chooseBackground(context: Context, imageStream: InputStream): Drawable {
|
||||||
val decoder = try {
|
val image = try {
|
||||||
ImageDecoder.new(imageStream)
|
ImageDecoder.new(imageStream).use { it.decode() }.let { res ->
|
||||||
|
createBitmap(res.width, res.height).also { bitmap ->
|
||||||
|
res.image.rewind()
|
||||||
|
bitmap.copyPixelsFromBuffer(res.image)
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR) { "chooseBackground: ${e.message}" }
|
logcat(LogPriority.ERROR) { "chooseBackground: ${e.message}" }
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
val result = decoder?.decode()
|
|
||||||
val image = result?.let {
|
|
||||||
createBitmap(it.width, it.height).also { bitmap ->
|
|
||||||
it.image.rewind()
|
|
||||||
bitmap.copyPixelsFromBuffer(it.image)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val whiteColor = Color.WHITE
|
val whiteColor = Color.WHITE
|
||||||
if (image == null) return whiteColor.toDrawable()
|
if (image == null) return whiteColor.toDrawable()
|
||||||
|
|||||||
@@ -39,11 +39,10 @@ firebase-bom = "34.19.0"
|
|||||||
firebase-crashlytics = "3.0.8"
|
firebase-crashlytics = "3.0.8"
|
||||||
flexibleAdapter = "5.1.0"
|
flexibleAdapter = "5.1.0"
|
||||||
google-services = "4.5.0"
|
google-services = "4.5.0"
|
||||||
image-decoder = "13"
|
image-decoder = "14"
|
||||||
injekt = "1.16.1"
|
injekt = "1.16.1"
|
||||||
jsoup = "1.23.2"
|
jsoup = "1.23.2"
|
||||||
junit = "6.1.3"
|
junit = "6.1.3"
|
||||||
kim = "0.40.0"
|
|
||||||
kotest-assertions = "6.2.5"
|
kotest-assertions = "6.2.5"
|
||||||
kotlin-gradle = "2.4.20"
|
kotlin-gradle = "2.4.20"
|
||||||
kotlinx-coroutines = "1.11.0"
|
kotlinx-coroutines = "1.11.0"
|
||||||
@@ -75,7 +74,7 @@ tapmoc = "0.4.2"
|
|||||||
unifile = "08f224c8f9"
|
unifile = "08f224c8f9"
|
||||||
valkyrie = "0.5.2"
|
valkyrie = "0.5.2"
|
||||||
voyager = "2.2.21-1.10.3"
|
voyager = "2.2.21-1.10.3"
|
||||||
webgpuviewer = "41"
|
webgpuviewer = "46"
|
||||||
xmlutil = "1.0.2"
|
xmlutil = "1.0.2"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
@@ -142,7 +141,6 @@ injekt = { module = "uy.kohesive.injekt:injekt-core", version.ref = "injekt" }
|
|||||||
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||||
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
||||||
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" }
|
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" }
|
||||||
kim = { module = "de.stefan-oltmann:kim", version.ref = "kim" }
|
|
||||||
kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest-assertions" }
|
kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest-assertions" }
|
||||||
kotlin-compose-compiler-gradle = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin-gradle" }
|
kotlin-compose-compiler-gradle = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin-gradle" }
|
||||||
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin-gradle" }
|
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin-gradle" }
|
||||||
|
|||||||
Reference in New Issue
Block a user