Encrypted CBZ archives (#846)

* Initial Implementation of encrypted CBZ archives

* changed a preference key to correct Syntax, changed a function name and changed ComicInfo padding length

* Update app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt

Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com>

* Update app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt

Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com>

* add necessary imports

* fix indentation after merge conflict

* Update app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt

Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com>

* fix indentation and add imports

* collect preferences as states

* test if password is correct in ZipPageLoader

* added withIOContext to function call

* added encryption type preference

* implemented database encryption

* added proguard rules for sqlcipher and generate padding length with SecureRandom

---------

Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com>
This commit is contained in:
Shamicen
2023-05-06 17:06:54 +02:00
committed by GitHub
parent 514e061dd9
commit 88f076afd4
22 changed files with 970 additions and 86 deletions
@@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.util.storage.CbzCrypto
import tachiyomi.core.metadata.comicinfo.ComicInfo
import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus
import tachiyomi.domain.chapter.model.Chapter
@@ -115,6 +116,9 @@ fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String) = ComicInfo
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
),
// SY -->
padding = CbzCrypto.createComicInfoPadding()?.let { ComicInfo.PaddingTachiyomiSY(it) },
// SY <--
inker = null,
colorist = null,
letterer = null,
@@ -1,28 +1,48 @@
package eu.kanade.presentation.more.settings.screen
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.fragment.app.FragmentActivity
@@ -34,10 +54,16 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
import eu.kanade.tachiyomi.ui.category.biometric.BiometricTimesScreen
import eu.kanade.tachiyomi.util.storage.CbzCrypto
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
import eu.kanade.tachiyomi.util.system.toast
import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
object SettingsSecurityScreen : SearchableSettings {
@@ -56,6 +82,10 @@ object SettingsSecurityScreen : SearchableSettings {
val useAuth by useAuthPref.collectAsState()
val scope = rememberCoroutineScope()
val isCbzPasswordSet by remember { CbzCrypto.isPasswordSetState(scope) }.collectAsState()
val passwordProtectDownloads by securityPreferences.passwordProtectDownloads().collectAsState()
return listOf(
Preference.PreferenceItem.SwitchPreference(
pref = useAuthPref,
@@ -96,6 +126,92 @@ object SettingsSecurityScreen : SearchableSettings {
.associateWith { stringResource(it.titleResId) },
),
// SY -->
Preference.PreferenceItem.SwitchPreference(
title = stringResource(R.string.encrypt_database),
pref = securityPreferences.encryptDatabase(),
subtitle = stringResource(R.string.encrypt_database_subtitle),
),
Preference.PreferenceItem.SwitchPreference(
pref = securityPreferences.passwordProtectDownloads(),
title = stringResource(R.string.password_protect_downloads),
subtitle = stringResource(R.string.password_protect_downloads_summary),
enabled = isCbzPasswordSet,
),
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.encryptionType(),
title = stringResource(R.string.encryption_type),
entries = SecurityPreferences.EncryptionType.values()
.associateWith { stringResource(it.titleResId) },
enabled = passwordProtectDownloads,
),
kotlin.run {
var dialogOpen by remember { mutableStateOf(false) }
if (dialogOpen) {
PasswordDialog(
onDismissRequest = { dialogOpen = false },
onReturnPassword = { password ->
dialogOpen = false
CbzCrypto.deleteKeyCbz()
securityPreferences.cbzPassword().set(CbzCrypto.encryptCbz(password.replace("\n", "")))
},
)
}
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.set_cbz_zip_password),
onClick = {
dialogOpen = true
},
)
},
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.delete_cbz_archive_password),
onClick = {
CbzCrypto.deleteKeyCbz()
securityPreferences.cbzPassword().set("")
},
enabled = isCbzPasswordSet,
),
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.localCoverLocation(),
title = stringResource(R.string.save_local_manga_covers),
entries = SecurityPreferences.CoverCacheLocation.values()
.associateWith { stringResource(it.titleResId) },
enabled = passwordProtectDownloads,
onValueChanged = {
try {
withIOContext {
CbzCrypto.deleteLocalCoverCache(context)
CbzCrypto.deleteLocalCoverSystemFiles(context)
}
true
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
context.toast(e.toString(), Toast.LENGTH_SHORT).show()
false
}
},
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.delete_cached_local_source_covers),
subtitle = stringResource(R.string.delete_cached_local_source_covers_subtitle),
onClick = {
try {
CbzCrypto.deleteLocalCoverCache(context)
CbzCrypto.deleteLocalCoverSystemFiles(context)
context.toast(R.string.successfully_deleted_all_locally_cached_covers, Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
context.toast(R.string.something_went_wrong_deleting_your_cover_images, Toast.LENGTH_LONG).show()
}
},
enabled = produceState(false) {
withIOContext {
value = context.getExternalFilesDir("covers/local")?.absolutePath?.let { File(it).listFiles()?.isNotEmpty() } == true
}
}.value,
),
kotlin.run {
val navigator = LocalNavigator.currentOrThrow
val count by securityPreferences.authenticatorTimeRanges().collectAsState()
@@ -215,6 +331,81 @@ object SettingsSecurityScreen : SearchableSettings {
},
)
}
@Composable
fun PasswordDialog(
onDismissRequest: () -> Unit,
onReturnPassword: (String) -> Unit,
) {
var password by rememberSaveable { mutableStateOf("") }
var passwordVisibility by remember { mutableStateOf(false) }
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.cbz_archive_password)) },
text = {
TextField(
value = password,
onValueChange = { password = it },
maxLines = 1,
placeholder = { Text(text = stringResource(R.string.password)) },
label = { Text(text = stringResource(R.string.password)) },
trailingIcon = {
IconButton(
onClick = {
passwordVisibility = !passwordVisibility
},
) {
Icon(
imageVector = if (passwordVisibility) {
Icons.Default.Visibility
} else {
Icons.Default.VisibilityOff
},
contentDescription = null,
)
}
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(
onDone = { onReturnPassword(password) },
),
modifier = Modifier.onKeyEvent {
if (it.key == Key.Enter) {
return@onKeyEvent true
}
false
},
visualTransformation = if (passwordVisibility) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
)
},
properties = DialogProperties(
usePlatformDefaultWidth = true,
),
confirmButton = {
TextButton(
onClick = {
onReturnPassword(password)
},
) {
Text(text = stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
}
},
)
}
// SY <--
}
@@ -27,11 +27,13 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.source.AndroidSourceManager
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.storage.CbzCrypto
import eu.kanade.tachiyomi.util.system.isDevFlavor
import exh.eh.EHentaiUpdateHelper
import exh.pref.DelegateSourcePreferences
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
import kotlinx.serialization.json.Json
import net.zetetic.database.sqlcipher.SupportOpenHelperFactory
import nl.adaptivity.xmlutil.XmlDeclMode
import nl.adaptivity.xmlutil.core.XmlVersion
import nl.adaptivity.xmlutil.serialization.UnknownChildHandler
@@ -64,23 +66,42 @@ import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingleton
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
// SY -->
private const val LEGACY_DATABASE_NAME = "tachiyomi.db"
// SY <--
class AppModule(val app: Application) : InjektModule {
// SY -->
private val securityPreferences: SecurityPreferences by injectLazy()
// SY <--
override fun InjektRegistrar.registerInjectables() {
addSingleton(app)
addSingletonFactory<SqlDriver> {
// SY -->
System.loadLibrary("sqlcipher")
// SY <--
AndroidSqliteDriver(
schema = Database.Schema,
context = app,
name = "tachiyomi.db",
factory = if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// SY -->
name = if (securityPreferences.encryptDatabase().get()) {
CbzCrypto.DATABASE_NAME
} else {
LEGACY_DATABASE_NAME
},
factory = if (securityPreferences.encryptDatabase().get()) {
SupportOpenHelperFactory(CbzCrypto.getDecryptedPasswordSql())
} else if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Support database inspector in Android Studio
FrameworkSQLiteOpenHelperFactory()
} else {
RequerySQLiteOpenHelperFactory()
},
// SY <--
callback = object : AndroidSqliteDriver.Callback(Database.Schema) {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
@@ -193,7 +214,7 @@ class PreferenceModule(val application: Application) : InjektModule {
SourcePreferences(get())
}
addSingletonFactory {
SecurityPreferences(get())
SecurityPreferences(get(), application.applicationContext)
}
addSingletonFactory {
LibraryPreferences(get())
@@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.storage.CbzCrypto
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
import eu.kanade.tachiyomi.util.storage.saveTo
@@ -34,6 +35,8 @@ import kotlinx.coroutines.flow.retryWhen
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.model.ZipParameters
import nl.adaptivity.xmlutil.serialization.XML
import okhttp3.Response
import rx.Observable
@@ -55,11 +58,7 @@ import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.BufferedOutputStream
import java.io.File
import java.util.zip.CRC32
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
/**
* This class is the one in charge of downloading chapters.
@@ -585,32 +584,42 @@ class Downloader(
dirname: String,
tmpDir: UniFile,
) {
val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")
ZipOutputStream(BufferedOutputStream(zip.openOutputStream())).use { zipOut ->
zipOut.setMethod(ZipEntry.STORED)
// SY -->
val zip = ZipFile("${mangaDir.filePath}/$dirname.cbz$TMP_DIR_SUFFIX")
val zipParameters = ZipParameters()
tmpDir.listFiles()?.forEach { img ->
img.openInputStream().use { input ->
val data = input.readBytes()
val size = img.length()
val entry = ZipEntry(img.name).apply {
val crc = CRC32().apply {
update(data)
}
setCrc(crc.value)
if (CbzCrypto.getPasswordProtectDlPref() &&
CbzCrypto.isPasswordSet()
) {
CbzCrypto.setZipParametersEncrypted(zipParameters)
zip.setPassword(CbzCrypto.getDecryptedPasswordCbz())
compressedSize = size
setSize(size)
}
zipOut.putNextEntry(entry)
zipOut.write(data)
}
}
tmpDir.filePath?.let { addPaddingToImage(File(it)) }
}
zip.renameTo("$dirname.cbz")
zip.addFiles(
tmpDir.listFiles()?.map { img -> img.filePath?.let { File(it) } },
zipParameters,
)
mangaDir.findFile("$dirname.cbz$TMP_DIR_SUFFIX")?.renameTo("$dirname.cbz")
// SY <--
tmpDir.delete()
}
// SY -->
private fun addPaddingToImage(imageDir: File) {
imageDir.listFiles()
// using ImageUtils isImage and findImageType functions causes IO errors when deleting files to set Exif Metadata
// it should be safe to assume that all files with image extensions are actual images at this point
?.filter {
it.extension.equals("jpg", true) ||
it.extension.equals("jpeg", true) ||
it.extension.equals("png", true) ||
it.extension.equals("webp", true)
}
?.forEach { ImageUtil.addPaddingToImageExif(it) }
}
// SY <--
/**
* Creates a ComicInfo.xml file inside the given directory.
*/
@@ -101,7 +101,13 @@ class ChapterLoader(
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
when (format) {
is Format.Directory -> DirectoryPageLoader(format.file)
is Format.Zip -> ZipPageLoader(format.file)
// SY -->
is Format.Zip -> try {
ZipPageLoader(format.file, context)
} catch (e: Throwable) {
error(context.getString(R.string.wrong_cbz_archive_password))
}
// SY <--
is Format.Rar -> try {
RarPageLoader(format.file)
} catch (e: UnsupportedRarV5Exception) {
@@ -119,7 +125,13 @@ class ChapterLoader(
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
when (format) {
is Format.Directory -> DirectoryPageLoader(format.file)
is Format.Zip -> ZipPageLoader(format.file)
// SY -->
is Format.Zip -> try {
ZipPageLoader(format.file, context)
} catch (e: Throwable) {
error(context.getString(R.string.wrong_cbz_archive_password))
}
// SY <--
is Format.Rar -> try {
RarPageLoader(format.file)
} catch (e: UnsupportedRarV5Exception) {
@@ -49,7 +49,9 @@ class DownloadPageLoader(
}
private suspend fun getPagesFromArchive(chapterPath: UniFile): List<ReaderPage> {
val loader = ZipPageLoader(File(chapterPath.filePath!!)).also { zipPageLoader = it }
// SY -->
val loader = ZipPageLoader(File(chapterPath.filePath!!), context).also { zipPageLoader = it }
// SY <--
return loader.getPages()
}
@@ -1,50 +1,98 @@
package eu.kanade.tachiyomi.ui.reader.loader
import android.content.Context
import android.os.Build
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.CbzCrypto
import net.lingala.zip4j.ZipFile
import tachiyomi.core.util.system.ImageUtil
import java.io.File
import java.nio.charset.StandardCharsets
import java.util.zip.ZipFile
/**
* Loader used to load a chapter from a .zip or .cbz file.
*/
class ZipPageLoader(file: File) : PageLoader() {
class ZipPageLoader(
file: File,
// SY -->
context: Context,
// SY <--
) : PageLoader() {
/**
* The zip file to load pages from.
*/
private val zip = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
ZipFile(file, StandardCharsets.ISO_8859_1)
} else {
ZipFile(file)
// SY -->
private var zip4j = ZipFile(file)
init {
if (zip4j.isEncrypted) {
if (!CbzCrypto.checkCbzPassword(zip4j, CbzCrypto.getDecryptedPasswordCbz())) {
this.recycle()
throw Exception(context.getString(R.string.wrong_cbz_archive_password))
}
}
}
private val zip: java.util.zip.ZipFile? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (!zip4j.isEncrypted) java.util.zip.ZipFile(file, StandardCharsets.ISO_8859_1) else null
} else {
if (!zip4j.isEncrypted) java.util.zip.ZipFile(file) else null
}
// SY <--
/**
* Recycles this loader and the open zip.
*/
override fun recycle() {
super.recycle()
zip.close()
// SY -->
zip4j.close()
zip?.close()
// SY <--
}
/**
* Returns the pages found on this zip archive ordered with a natural comparator.
*/
override suspend fun getPages(): List<ReaderPage> {
return zip.entries().asSequence()
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.mapIndexed { i, entry ->
ReaderPage(i).apply {
stream = { zip.getInputStream(entry) }
status = Page.State.READY
}
// SY -->
// Part can be removed after testing that there are no bugs with zip4j on some users devices
if (zip != null) {
// SY <--
return zip.entries().asSequence()
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.mapIndexed { i, entry ->
ReaderPage(i).apply {
stream = { zip.getInputStream(entry) }
status = Page.State.READY
}
// SY -->
}.toList()
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
zip4j.charset = StandardCharsets.ISO_8859_1
}
.toList()
zip4j.setPassword(CbzCrypto.getDecryptedPasswordCbz())
return zip4j.fileHeaders.asSequence()
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { zip4j.getInputStream(it) } }
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.mapIndexed { i, entry ->
ReaderPage(i).apply {
stream = { zip4j.getInputStream(entry) }
status = Page.State.READY
zip4jFile = zip4j
zip4jEntry = entry
}
}.toList()
}
// SY <--
}
/**
@@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.ui.reader.model
import eu.kanade.tachiyomi.source.model.Page
import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.model.FileHeader
import java.io.InputStream
open class ReaderPage(
@@ -8,6 +10,9 @@ open class ReaderPage(
url: String = "",
imageUrl: String? = null,
// SY -->
/**zip4j inputStreams do not support mark() and release(), so they must be passed to ImageUtil */
var zip4jFile: ZipFile? = null,
var zip4jEntry: FileHeader? = null,
/** Value to check if this page is used to as if it was too wide */
var shiftedPage: Boolean = false,
/** Value to check if a page is can be doubled up, but can't because the next page is too wide */
@@ -168,7 +168,14 @@ class PagerPageHolder(
val (bais, isAnimated, background) = withIOContext {
streamFn().buffered(16).use { stream ->
// SY -->
(if (extraPage != null) streamFn2?.invoke()?.buffered(16) else null).use { stream2 ->
(
if (extraPage != null) {
streamFn2?.invoke()
?.buffered(16)
} else {
null
}
).use { stream2 ->
if (viewer.config.dualPageSplit) {
process(item.first, stream)
} else {
@@ -222,7 +229,13 @@ class PagerPageHolder(
return splitInHalf(imageStream)
}
val isDoublePage = ImageUtil.isWideImage(imageStream)
val isDoublePage = ImageUtil.isWideImage(
imageStream,
// SY -->
page.zip4jFile,
page.zip4jEntry,
// SY <--
)
if (!isDoublePage) {
return imageStream
}
@@ -247,7 +260,13 @@ class PagerPageHolder(
if (imageStream2 == null) {
return if (imageStream is BufferedInputStream &&
!ImageUtil.isAnimatedAndSupported(imageStream) &&
ImageUtil.isWideImage(imageStream) &&
ImageUtil.isWideImage(
imageStream,
// SY -->
page.zip4jFile,
page.zip4jEntry,
// SY <--
) &&
viewer.config.centerMarginType and PagerConfig.CenterMarginType.WIDE_PAGE_CENTER_MARGIN > 0 &&
!viewer.config.imageCropBorders
) {
@@ -213,7 +213,13 @@ class WebtoonPageHolder(
private fun process(imageStream: BufferedInputStream): InputStream {
if (viewer.config.dualPageSplit) {
val isDoublePage = ImageUtil.isWideImage(imageStream)
val isDoublePage = ImageUtil.isWideImage(
imageStream,
// SY -->
page?.zip4jFile,
page?.zip4jEntry,
// SY <--
)
if (isDoublePage) {
val upperSide = if (viewer.config.dualPageInvert) ImageUtil.Side.LEFT else ImageUtil.Side.RIGHT
return ImageUtil.splitAndMerge(imageStream, upperSide)
@@ -224,7 +230,13 @@ class WebtoonPageHolder(
if (page is StencilPage) {
return imageStream
}
val isStripSplitNeeded = ImageUtil.isStripSplitNeeded(imageStream)
val isStripSplitNeeded = ImageUtil.isStripSplitNeeded(
imageStream,
// SY -->
page?.zip4jFile,
page?.zip4jEntry,
// SY <--
)
if (isStripSplitNeeded) {
return onStripSplit(imageStream)
}
@@ -237,7 +249,14 @@ class WebtoonPageHolder(
// If we have reached this point [page] and its stream shouldn't be null
val page = page!!
val stream = page.stream!!
val splitData = ImageUtil.getSplitDataForStream(imageStream).toMutableList()
val splitData = ImageUtil.getSplitDataForStream(
imageStream,
// SY -->
page.zip4jFile,
page.zip4jEntry,
// SY <--
).toMutableList()
val currentSplitData = splitData.removeFirst()
val newPages = splitData.map {
StencilPage(page) { ImageUtil.splitStrip(it, stream) }