mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-21 20:47:03 -05:00
Long Strip Split for Webtoon (#5759)
* Long Strip Split for Webtoon * Review Changes * Review Changes 2 + Rebase
This commit is contained in:
parent
d6c0a5ef8b
commit
88b56121a3
11 changed files with 218 additions and 44 deletions
|
@ -83,12 +83,14 @@ class PreferencesHelper(val context: Context) {
|
||||||
|
|
||||||
fun dualPageSplitPaged() = flowPrefs.getBoolean("pref_dual_page_split", false)
|
fun dualPageSplitPaged() = flowPrefs.getBoolean("pref_dual_page_split", false)
|
||||||
|
|
||||||
fun dualPageSplitWebtoon() = flowPrefs.getBoolean("pref_dual_page_split_webtoon", false)
|
|
||||||
|
|
||||||
fun dualPageInvertPaged() = flowPrefs.getBoolean("pref_dual_page_invert", false)
|
fun dualPageInvertPaged() = flowPrefs.getBoolean("pref_dual_page_invert", false)
|
||||||
|
|
||||||
|
fun dualPageSplitWebtoon() = flowPrefs.getBoolean("pref_dual_page_split_webtoon", false)
|
||||||
|
|
||||||
fun dualPageInvertWebtoon() = flowPrefs.getBoolean("pref_dual_page_invert_webtoon", false)
|
fun dualPageInvertWebtoon() = flowPrefs.getBoolean("pref_dual_page_invert_webtoon", false)
|
||||||
|
|
||||||
|
fun longStripSplitWebtoon() = flowPrefs.getBoolean("pref_long_strip_split_webtoon", true)
|
||||||
|
|
||||||
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
|
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
|
||||||
|
|
||||||
fun trueColor() = flowPrefs.getBoolean("pref_true_color_key", false)
|
fun trueColor() = flowPrefs.getBoolean("pref_true_color_key", false)
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.reader.model
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
|
|
||||||
|
class StencilPage(
|
||||||
|
parent: ReaderPage,
|
||||||
|
val splitData: ImageUtil.SplitData,
|
||||||
|
) : ReaderPage(parent.index, parent.url, parent.imageUrl) {
|
||||||
|
|
||||||
|
override var chapter: ReaderChapter = parent.chapter
|
||||||
|
|
||||||
|
init {
|
||||||
|
stream = parent.stream
|
||||||
|
}
|
||||||
|
}
|
|
@ -86,8 +86,8 @@ class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attr
|
||||||
binding.pagerPrefsGroup.cropBorders.bindToPreference(preferences.cropBorders())
|
binding.pagerPrefsGroup.cropBorders.bindToPreference(preferences.cropBorders())
|
||||||
binding.pagerPrefsGroup.navigatePan.bindToPreference(preferences.navigateToPan())
|
binding.pagerPrefsGroup.navigatePan.bindToPreference(preferences.navigateToPan())
|
||||||
|
|
||||||
// Makes so that dual page invert gets hidden away when turning of dual page split
|
|
||||||
binding.pagerPrefsGroup.dualPageSplit.bindToPreference(preferences.dualPageSplitPaged())
|
binding.pagerPrefsGroup.dualPageSplit.bindToPreference(preferences.dualPageSplitPaged())
|
||||||
|
// Makes it so that dual page invert gets hidden away when dual page split is turned off
|
||||||
preferences.dualPageSplitPaged()
|
preferences.dualPageSplitPaged()
|
||||||
.asHotFlow { binding.pagerPrefsGroup.dualPageInvert.isVisible = it }
|
.asHotFlow { binding.pagerPrefsGroup.dualPageInvert.isVisible = it }
|
||||||
.launchIn((context as ReaderActivity).lifecycleScope)
|
.launchIn((context as ReaderActivity).lifecycleScope)
|
||||||
|
@ -110,11 +110,12 @@ class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attr
|
||||||
binding.webtoonPrefsGroup.cropBordersWebtoon.bindToPreference(preferences.cropBordersWebtoon())
|
binding.webtoonPrefsGroup.cropBordersWebtoon.bindToPreference(preferences.cropBordersWebtoon())
|
||||||
binding.webtoonPrefsGroup.webtoonSidePadding.bindToIntPreference(preferences.webtoonSidePadding(), R.array.webtoon_side_padding_values)
|
binding.webtoonPrefsGroup.webtoonSidePadding.bindToIntPreference(preferences.webtoonSidePadding(), R.array.webtoon_side_padding_values)
|
||||||
|
|
||||||
// Makes so that dual page invert gets hidden away when turning of dual page split
|
|
||||||
binding.webtoonPrefsGroup.dualPageSplit.bindToPreference(preferences.dualPageSplitWebtoon())
|
binding.webtoonPrefsGroup.dualPageSplit.bindToPreference(preferences.dualPageSplitWebtoon())
|
||||||
|
// Makes it so that dual page invert gets hidden away when dual page split is turned off
|
||||||
preferences.dualPageSplitWebtoon()
|
preferences.dualPageSplitWebtoon()
|
||||||
.asHotFlow { binding.webtoonPrefsGroup.dualPageInvert.isVisible = it }
|
.asHotFlow { binding.webtoonPrefsGroup.dualPageInvert.isVisible = it }
|
||||||
.launchIn((context as ReaderActivity).lifecycleScope)
|
.launchIn((context as ReaderActivity).lifecycleScope)
|
||||||
binding.webtoonPrefsGroup.dualPageInvert.bindToPreference(preferences.dualPageInvertWebtoon())
|
binding.webtoonPrefsGroup.dualPageInvert.bindToPreference(preferences.dualPageInvertWebtoon())
|
||||||
|
binding.webtoonPrefsGroup.longStripSplit.bindToPreference(preferences.longStripSplitWebtoon())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,12 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.model.StencilPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.hasMissingChapters
|
import eu.kanade.tachiyomi.ui.reader.viewer.hasMissingChapters
|
||||||
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
|
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RecyclerView Adapter used by this [viewer] to where [ViewerChapters] updates are posted.
|
* RecyclerView Adapter used by this [viewer] to where [ViewerChapters] updates are posted.
|
||||||
|
@ -25,6 +27,26 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
|
||||||
|
|
||||||
var currentChapter: ReaderChapter? = null
|
var currentChapter: ReaderChapter? = null
|
||||||
|
|
||||||
|
fun onLongStripSplit(currentStrip: Any?, newStrips: List<StencilPage>) {
|
||||||
|
if (currentStrip is StencilPage) return
|
||||||
|
|
||||||
|
val placeAtIndex = items.indexOf(currentStrip) + 1
|
||||||
|
// Stop constantly adding split images
|
||||||
|
if (items[placeAtIndex] is StencilPage) return
|
||||||
|
|
||||||
|
val updatedItems = items.toMutableList()
|
||||||
|
updatedItems.addAll(placeAtIndex, newStrips)
|
||||||
|
updateItems(updatedItems)
|
||||||
|
logcat { "New adapter item count is $itemCount" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cleanupSplitStrips() {
|
||||||
|
if (items.any { it is StencilPage }) {
|
||||||
|
val updatedItems = items.filterNot { it is StencilPage }
|
||||||
|
updateItems(updatedItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context that has been wrapped to use the correct theme values based on the
|
* Context that has been wrapped to use the correct theme values based on the
|
||||||
* current app theme and reader background color
|
* current app theme and reader background color
|
||||||
|
@ -79,6 +101,10 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateItems(newItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateItems(newItems: List<Any>) {
|
||||||
val result = DiffUtil.calculateDiff(Callback(items, newItems))
|
val result = DiffUtil.calculateDiff(Callback(items, newItems))
|
||||||
items = newItems
|
items = newItems
|
||||||
result.dispatchUpdatesTo(this)
|
result.dispatchUpdatesTo(this)
|
||||||
|
|
|
@ -32,6 +32,11 @@ class WebtoonConfig(
|
||||||
var sidePadding = 0
|
var sidePadding = 0
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var longStripSplit = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
var longStripSplitChangedListener: ((Boolean) -> Unit)? = null
|
||||||
|
|
||||||
val theme = preferences.readerTheme().get()
|
val theme = preferences.readerTheme().get()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -57,6 +62,15 @@ class WebtoonConfig(
|
||||||
preferences.dualPageInvertWebtoon()
|
preferences.dualPageInvertWebtoon()
|
||||||
.register({ dualPageInvert = it }, { imagePropertyChangedListener?.invoke() })
|
.register({ dualPageInvert = it }, { imagePropertyChangedListener?.invoke() })
|
||||||
|
|
||||||
|
preferences.longStripSplitWebtoon()
|
||||||
|
.register(
|
||||||
|
{ longStripSplit = it },
|
||||||
|
{
|
||||||
|
imagePropertyChangedListener?.invoke()
|
||||||
|
longStripSplitChangedListener?.invoke(it)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
preferences.readerTheme().asFlow()
|
preferences.readerTheme().asFlow()
|
||||||
.drop(1)
|
.drop(1)
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
|
|
@ -14,10 +14,12 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import eu.kanade.tachiyomi.databinding.ReaderErrorBinding
|
import eu.kanade.tachiyomi.databinding.ReaderErrorBinding
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.model.StencilPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
||||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.ImageUtil.SplitData
|
||||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
|
@ -274,17 +276,37 @@ class WebtoonPageHolder(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun process(imageStream: BufferedInputStream): InputStream {
|
private fun process(imageStream: BufferedInputStream): InputStream {
|
||||||
if (!viewer.config.dualPageSplit) {
|
if (viewer.config.dualPageSplit) {
|
||||||
return imageStream
|
val isDoublePage = ImageUtil.isWideImage(imageStream)
|
||||||
|
if (isDoublePage) {
|
||||||
|
val upperSide = if (viewer.config.dualPageInvert) ImageUtil.Side.LEFT else ImageUtil.Side.RIGHT
|
||||||
|
return ImageUtil.splitAndMerge(imageStream, upperSide)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isDoublePage = ImageUtil.isWideImage(imageStream)
|
if (viewer.config.longStripSplit) {
|
||||||
if (!isDoublePage) {
|
if (page is StencilPage) {
|
||||||
return imageStream
|
val splitData = (page as StencilPage).splitData
|
||||||
|
return ImageUtil.splitStrip(imageStream, splitData)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isStripSplitNeeded = ImageUtil.isStripSplitNeeded(imageStream)
|
||||||
|
if (isStripSplitNeeded) {
|
||||||
|
val splitData = onStripSplit(imageStream)
|
||||||
|
splitData?.let { return ImageUtil.splitStrip(imageStream, it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val upperSide = if (viewer.config.dualPageInvert) ImageUtil.Side.LEFT else ImageUtil.Side.RIGHT
|
return imageStream
|
||||||
return ImageUtil.splitAndMerge(imageStream, upperSide)
|
}
|
||||||
|
|
||||||
|
private fun onStripSplit(imageStream: BufferedInputStream): SplitData? {
|
||||||
|
val page = page ?: return null
|
||||||
|
val splitData = ImageUtil.getSplitDataForStream(imageStream).toMutableList()
|
||||||
|
val toReturn = splitData.removeFirstOrNull()
|
||||||
|
val newPages = splitData.map { StencilPage(page, it) }
|
||||||
|
viewer.onLongStripSplit(page, newPages)
|
||||||
|
return toReturn
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.model.StencilPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
|
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
|
||||||
|
@ -154,6 +155,12 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
||||||
activity.binding.navigationOverlay.setNavigation(config.navigator, showOnStart)
|
activity.binding.navigationOverlay.setNavigation(config.navigator, showOnStart)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.longStripSplitChangedListener = { enabled ->
|
||||||
|
if (!enabled) {
|
||||||
|
cleanupSplitStrips()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
frame.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
frame.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
||||||
frame.addView(recycler)
|
frame.addView(recycler)
|
||||||
}
|
}
|
||||||
|
@ -354,4 +361,15 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
||||||
min(position + 3, adapter.itemCount - 1),
|
min(position + 3, adapter.itemCount - 1),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onLongStripSplit(currentStrip: Any?, newStrips: List<StencilPage>) {
|
||||||
|
activity.runOnUiThread {
|
||||||
|
// Need to insert on UI thread else images will go blank
|
||||||
|
adapter.onLongStripSplit(currentStrip, newStrips)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanupSplitStrips() {
|
||||||
|
adapter.cleanupSplitStrips()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -289,6 +289,11 @@ class SettingsReaderController : SettingsController() {
|
||||||
summaryRes = R.string.pref_dual_page_invert_summary
|
summaryRes = R.string.pref_dual_page_invert_summary
|
||||||
visibleIf(preferences.dualPageSplitWebtoon()) { it }
|
visibleIf(preferences.dualPageSplitWebtoon()) { it }
|
||||||
}
|
}
|
||||||
|
switchPreference {
|
||||||
|
bindTo(preferences.longStripSplitWebtoon())
|
||||||
|
titleRes = R.string.pref_long_strip_split
|
||||||
|
summaryRes = R.string.pref_long_strip_split_summary
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
preferenceCategory {
|
preferenceCategory {
|
||||||
|
|
|
@ -206,35 +206,6 @@ object ImageUtil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
val options = extractImageOptions(imageFile.openInputStream(), resetAfterExtraction = false).apply { inJustDecodeBounds = false }
|
|
||||||
// Values are stored as they get modified during split loop
|
|
||||||
val imageHeight = options.outHeight
|
|
||||||
val imageWidth = options.outWidth
|
|
||||||
|
|
||||||
val splitHeight = (getDisplayMaxHeightInPx * 1.5).toInt()
|
|
||||||
// -1 so it doesn't try to split when imageHeight = getDisplayHeightInPx
|
|
||||||
val partCount = (imageHeight - 1) / splitHeight + 1
|
|
||||||
|
|
||||||
val optimalSplitHeight = imageHeight / partCount
|
|
||||||
|
|
||||||
val splitDataList = (0 until partCount).fold(mutableListOf<SplitData>()) { list, index ->
|
|
||||||
list.apply {
|
|
||||||
// Only continue if the list is empty or there is image remaining
|
|
||||||
if (isEmpty() || imageHeight > last().bottomOffset) {
|
|
||||||
val topOffset = index * optimalSplitHeight
|
|
||||||
var outputImageHeight = min(optimalSplitHeight, imageHeight - topOffset)
|
|
||||||
|
|
||||||
val remainingHeight = imageHeight - (topOffset + outputImageHeight)
|
|
||||||
// If remaining height is smaller or equal to 1/3th of
|
|
||||||
// optimal split height then include it in current page
|
|
||||||
if (remainingHeight <= (optimalSplitHeight / 3)) {
|
|
||||||
outputImageHeight += remainingHeight
|
|
||||||
}
|
|
||||||
add(SplitData(index, topOffset, outputImageHeight))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val bitmapRegionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
val bitmapRegionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
BitmapRegionDecoder.newInstance(imageFile.openInputStream())
|
BitmapRegionDecoder.newInstance(imageFile.openInputStream())
|
||||||
} else {
|
} else {
|
||||||
|
@ -247,10 +218,12 @@ object ImageUtil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
logcat {
|
val options = extractImageOptions(imageFile.openInputStream(), resetAfterExtraction = false).apply { inJustDecodeBounds = false }
|
||||||
"Splitting image with height of $imageHeight into $partCount part " +
|
|
||||||
"with estimated ${optimalSplitHeight}px height per split"
|
// Values are stored as they get modified during split loop
|
||||||
}
|
val imageWidth = options.outWidth
|
||||||
|
|
||||||
|
val splitDataList = getSplitDataForOptions(options)
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
splitDataList.forEach { splitData ->
|
splitDataList.forEach { splitData ->
|
||||||
|
@ -285,6 +258,93 @@ object ImageUtil {
|
||||||
private fun splitImagePath(imageFilePath: String, index: Int) =
|
private fun splitImagePath(imageFilePath: String, index: Int) =
|
||||||
imageFilePath.substringBeforeLast(".") + "__${"%03d".format(index + 1)}.jpg"
|
imageFilePath.substringBeforeLast(".") + "__${"%03d".format(index + 1)}.jpg"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the image is a long Strip that needs splitting
|
||||||
|
* @return true if the image is not animated and it's height is greater than image width and screen height
|
||||||
|
*/
|
||||||
|
fun isStripSplitNeeded(imageStream: BufferedInputStream): Boolean {
|
||||||
|
if (isAnimatedAndSupported(imageStream)) return false
|
||||||
|
val options = extractImageOptions(imageStream)
|
||||||
|
|
||||||
|
val imageHeightIsBiggerThanWidth = options.outHeight > options.outWidth
|
||||||
|
val imageHeightBiggerThanScreenHeight = options.outHeight > getDisplayMaxHeightInPx
|
||||||
|
return imageHeightIsBiggerThanWidth && imageHeightBiggerThanScreenHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split the imageStream according to the provided splitData
|
||||||
|
*/
|
||||||
|
fun splitStrip(imageStream: InputStream, splitData: SplitData): InputStream {
|
||||||
|
val bitmapRegionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
BitmapRegionDecoder.newInstance(imageStream)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
BitmapRegionDecoder.newInstance(imageStream, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bitmapRegionDecoder == null) {
|
||||||
|
throw Exception("Failed to create new instance of BitmapRegionDecoder")
|
||||||
|
}
|
||||||
|
|
||||||
|
logcat {
|
||||||
|
"WebtoonSplit #${splitData.index} with topOffset=${splitData.topOffset} " +
|
||||||
|
"outputImageHeight=${splitData.outputImageHeight} bottomOffset=${splitData.bottomOffset}"
|
||||||
|
}
|
||||||
|
|
||||||
|
val options = extractImageOptions(imageStream).apply { inJustDecodeBounds = false }
|
||||||
|
|
||||||
|
val region = Rect(0, splitData.topOffset, splitData.outputImageHeight, splitData.bottomOffset)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val splitBitmap = bitmapRegionDecoder.decodeRegion(region, options)
|
||||||
|
val outputStream = ByteArrayOutputStream()
|
||||||
|
splitBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||||
|
return ByteArrayInputStream(outputStream.toByteArray())
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
bitmapRegionDecoder.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSplitDataForStream(imageStream: InputStream): List<SplitData> {
|
||||||
|
val options = extractImageOptions(imageStream)
|
||||||
|
return getSplitDataForOptions(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSplitDataForOptions(options: BitmapFactory.Options): List<SplitData> {
|
||||||
|
val imageHeight = options.outHeight
|
||||||
|
|
||||||
|
val splitHeight = (getDisplayMaxHeightInPx * 1.5).toInt()
|
||||||
|
// -1 so it doesn't try to split when imageHeight = splitHeight
|
||||||
|
val partCount = (imageHeight - 1) / splitHeight + 1
|
||||||
|
|
||||||
|
val optimalSplitHeight = imageHeight / partCount
|
||||||
|
|
||||||
|
logcat {
|
||||||
|
"Generating SplitData for image with height of $imageHeight. " +
|
||||||
|
"Estimated $partCount part and ${optimalSplitHeight}px height per part"
|
||||||
|
}
|
||||||
|
|
||||||
|
return mutableListOf<SplitData>().apply {
|
||||||
|
for (index in (0 until partCount)) {
|
||||||
|
// Only continue if the list is empty or there is image remaining
|
||||||
|
if (isNotEmpty() && imageHeight <= last().bottomOffset) break
|
||||||
|
|
||||||
|
val topOffset = index * optimalSplitHeight
|
||||||
|
var outputImageHeight = min(optimalSplitHeight, imageHeight - topOffset)
|
||||||
|
|
||||||
|
val remainingHeight = imageHeight - (topOffset + outputImageHeight)
|
||||||
|
// If remaining height is smaller or equal to 1/10th of
|
||||||
|
// optimal split height then include it in current page
|
||||||
|
if (remainingHeight <= (optimalSplitHeight / 10)) {
|
||||||
|
outputImageHeight += remainingHeight
|
||||||
|
}
|
||||||
|
add(SplitData(index, topOffset, outputImageHeight))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class SplitData(
|
data class SplitData(
|
||||||
val index: Int,
|
val index: Int,
|
||||||
val topOffset: Int,
|
val topOffset: Int,
|
||||||
|
|
|
@ -66,6 +66,15 @@
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/long_strip_split"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:text="@string/pref_long_strip_split"
|
||||||
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Group
|
<androidx.constraintlayout.widget.Group
|
||||||
android:id="@+id/tapping_prefs_group"
|
android:id="@+id/tapping_prefs_group"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|
|
@ -297,6 +297,8 @@
|
||||||
<string name="pref_dual_page_split">Dual page split</string>
|
<string name="pref_dual_page_split">Dual page split</string>
|
||||||
<string name="pref_dual_page_invert">Invert dual page split placement</string>
|
<string name="pref_dual_page_invert">Invert dual page split placement</string>
|
||||||
<string name="pref_dual_page_invert_summary">If the placement of the dual page split doesn\'t match reading direction</string>
|
<string name="pref_dual_page_invert_summary">If the placement of the dual page split doesn\'t match reading direction</string>
|
||||||
|
<string name="pref_long_strip_split">Split tall images (Alpha)</string>
|
||||||
|
<string name="pref_long_strip_split_summary">Improves reader performance</string>
|
||||||
<string name="pref_cutout_short">Show content in cutout area</string>
|
<string name="pref_cutout_short">Show content in cutout area</string>
|
||||||
<string name="pref_page_transitions">Animate page transitions</string>
|
<string name="pref_page_transitions">Animate page transitions</string>
|
||||||
<string name="pref_double_tap_anim_speed">Double tap animation speed</string>
|
<string name="pref_double_tap_anim_speed">Double tap animation speed</string>
|
||||||
|
|
Loading…
Reference in a new issue