mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01:00 
			
		
		
		
	Long Strip Split for Webtoon (#5759)
* Long Strip Split for Webtoon * Review Changes * Review Changes 2 + Rebase
This commit is contained in:
		| @@ -83,12 +83,14 @@ class PreferencesHelper(val context: Context) { | ||||
|  | ||||
|     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 dualPageSplitWebtoon() = flowPrefs.getBoolean("pref_dual_page_split_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 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.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()) | ||||
|         // Makes it so that dual page invert gets hidden away when dual page split is turned off | ||||
|         preferences.dualPageSplitPaged() | ||||
|             .asHotFlow { binding.pagerPrefsGroup.dualPageInvert.isVisible = it } | ||||
|             .launchIn((context as ReaderActivity).lifecycleScope) | ||||
| @@ -110,11 +110,12 @@ class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attr | ||||
|         binding.webtoonPrefsGroup.cropBordersWebtoon.bindToPreference(preferences.cropBordersWebtoon()) | ||||
|         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()) | ||||
|         // Makes it so that dual page invert gets hidden away when dual page split is turned off | ||||
|         preferences.dualPageSplitWebtoon() | ||||
|             .asHotFlow { binding.webtoonPrefsGroup.dualPageInvert.isVisible = it } | ||||
|             .launchIn((context as ReaderActivity).lifecycleScope) | ||||
|         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.ReaderChapter | ||||
| 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.viewer.ReaderPageImageView | ||||
| import eu.kanade.tachiyomi.ui.reader.viewer.hasMissingChapters | ||||
| 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. | ||||
| @@ -25,6 +27,26 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV | ||||
|  | ||||
|     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 | ||||
|      * 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)) | ||||
|         items = newItems | ||||
|         result.dispatchUpdatesTo(this) | ||||
|   | ||||
| @@ -32,6 +32,11 @@ class WebtoonConfig( | ||||
|     var sidePadding = 0 | ||||
|         private set | ||||
|  | ||||
|     var longStripSplit = false | ||||
|         private set | ||||
|  | ||||
|     var longStripSplitChangedListener: ((Boolean) -> Unit)? = null | ||||
|  | ||||
|     val theme = preferences.readerTheme().get() | ||||
|  | ||||
|     init { | ||||
| @@ -57,6 +62,15 @@ class WebtoonConfig( | ||||
|         preferences.dualPageInvertWebtoon() | ||||
|             .register({ dualPageInvert = it }, { imagePropertyChangedListener?.invoke() }) | ||||
|  | ||||
|         preferences.longStripSplitWebtoon() | ||||
|             .register( | ||||
|                 { longStripSplit = it }, | ||||
|                 { | ||||
|                     imagePropertyChangedListener?.invoke() | ||||
|                     longStripSplitChangedListener?.invoke(it) | ||||
|                 }, | ||||
|             ) | ||||
|  | ||||
|         preferences.readerTheme().asFlow() | ||||
|             .drop(1) | ||||
|             .distinctUntilChanged() | ||||
|   | ||||
| @@ -14,10 +14,12 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView | ||||
| import eu.kanade.tachiyomi.databinding.ReaderErrorBinding | ||||
| import eu.kanade.tachiyomi.source.model.Page | ||||
| 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.ReaderProgressIndicator | ||||
| import eu.kanade.tachiyomi.ui.webview.WebViewActivity | ||||
| import eu.kanade.tachiyomi.util.system.ImageUtil | ||||
| import eu.kanade.tachiyomi.util.system.ImageUtil.SplitData | ||||
| import eu.kanade.tachiyomi.util.system.dpToPx | ||||
| import rx.Observable | ||||
| import rx.Subscription | ||||
| @@ -274,17 +276,37 @@ class WebtoonPageHolder( | ||||
|     } | ||||
|  | ||||
|     private fun process(imageStream: BufferedInputStream): InputStream { | ||||
|         if (!viewer.config.dualPageSplit) { | ||||
|             return imageStream | ||||
|         if (viewer.config.dualPageSplit) { | ||||
|             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 (!isDoublePage) { | ||||
|             return imageStream | ||||
|         if (viewer.config.longStripSplit) { | ||||
|             if (page is StencilPage) { | ||||
|                 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 ImageUtil.splitAndMerge(imageStream, upperSide) | ||||
|         return imageStream | ||||
|     } | ||||
|  | ||||
|     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.model.ChapterTransition | ||||
| 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.viewer.BaseViewer | ||||
| 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) | ||||
|         } | ||||
|  | ||||
|         config.longStripSplitChangedListener = { enabled -> | ||||
|             if (!enabled) { | ||||
|                 cleanupSplitStrips() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         frame.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) | ||||
|         frame.addView(recycler) | ||||
|     } | ||||
| @@ -354,4 +361,15 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr | ||||
|             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 | ||||
|                 visibleIf(preferences.dualPageSplitWebtoon()) { it } | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(preferences.longStripSplitWebtoon()) | ||||
|                 titleRes = R.string.pref_long_strip_split | ||||
|                 summaryRes = R.string.pref_long_strip_split_summary | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|   | ||||
| @@ -206,35 +206,6 @@ object ImageUtil { | ||||
|             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) { | ||||
|             BitmapRegionDecoder.newInstance(imageFile.openInputStream()) | ||||
|         } else { | ||||
| @@ -247,10 +218,12 @@ object ImageUtil { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         logcat { | ||||
|             "Splitting image with height of $imageHeight into $partCount part " + | ||||
|                 "with estimated ${optimalSplitHeight}px height per split" | ||||
|         } | ||||
|         val options = extractImageOptions(imageFile.openInputStream(), resetAfterExtraction = false).apply { inJustDecodeBounds = false } | ||||
|  | ||||
|         // Values are stored as they get modified during split loop | ||||
|         val imageWidth = options.outWidth | ||||
|  | ||||
|         val splitDataList = getSplitDataForOptions(options) | ||||
|  | ||||
|         return try { | ||||
|             splitDataList.forEach { splitData -> | ||||
| @@ -285,6 +258,93 @@ object ImageUtil { | ||||
|     private fun splitImagePath(imageFilePath: String, index: Int) = | ||||
|         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( | ||||
|         val index: Int, | ||||
|         val topOffset: Int, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user