Load ZIP file contents to cache (#9381)
* Extract downloaded archives to tmp folder when loading for viewing * Generate sequence of entries from ZipInputStream instead of loading entire ZipFile (cherry picked from commit 44619febd333f4e662cdbf149ae0741a43ebd27b) # Conflicts: # app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt
This commit is contained in:
parent
cf752a0d88
commit
20d35268b1
@ -121,7 +121,6 @@ class ChapterLoader(
|
||||
}
|
||||
// SY <--
|
||||
isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager, downloadProvider)
|
||||
source is HttpSource -> HttpPageLoader(chapter, source)
|
||||
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
||||
when (format) {
|
||||
is Format.Directory -> DirectoryPageLoader(format.file)
|
||||
@ -140,6 +139,7 @@ class ChapterLoader(
|
||||
is Format.Epub -> EpubPageLoader(format.file)
|
||||
}
|
||||
}
|
||||
source is HttpSource -> HttpPageLoader(chapter, source)
|
||||
source is StubSource -> error(context.getString(R.string.source_not_installed, source.toString()))
|
||||
else -> error(context.getString(R.string.loader_not_implemented_error))
|
||||
}
|
||||
|
@ -1,61 +1,57 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.loader
|
||||
|
||||
import android.app.Application
|
||||
import com.github.junrar.Archive
|
||||
import com.github.junrar.rarfile.FileHeader
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.PipedInputStream
|
||||
import java.io.PipedOutputStream
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Loader used to load a chapter from a .rar or .cbr file.
|
||||
*/
|
||||
internal class RarPageLoader(file: File) : PageLoader() {
|
||||
|
||||
private val rar = Archive(file)
|
||||
private val context: Application by injectLazy()
|
||||
private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also {
|
||||
it.deleteRecursively()
|
||||
it.mkdirs()
|
||||
}
|
||||
|
||||
/**
|
||||
* Pool for copying compressed files to an input stream.
|
||||
*/
|
||||
private val pool = Executors.newFixedThreadPool(1)
|
||||
init {
|
||||
Archive(file).use { rar ->
|
||||
rar.fileHeaders.asSequence()
|
||||
.filterNot { it.isDirectory }
|
||||
.forEach { header ->
|
||||
val pageFile = File(tmpDir, header.fileName).also { it.createNewFile() }
|
||||
getStream(rar, header).use {
|
||||
it.copyTo(pageFile.outputStream())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var isLocal: Boolean = true
|
||||
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
return rar.fileHeaders.asSequence()
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } }
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.mapIndexed { i, header ->
|
||||
ReaderPage(i).apply {
|
||||
stream = { getStream(header) }
|
||||
status = Page.State.READY
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
override suspend fun loadPage(page: ReaderPage) {
|
||||
check(!isRecycled)
|
||||
return DirectoryPageLoader(tmpDir).getPages()
|
||||
}
|
||||
|
||||
override fun recycle() {
|
||||
super.recycle()
|
||||
rar.close()
|
||||
pool.shutdown()
|
||||
tmpDir.deleteRecursively()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an input stream for the given [header].
|
||||
*/
|
||||
private fun getStream(header: FileHeader): InputStream {
|
||||
private fun getStream(rar: Archive, header: FileHeader): InputStream {
|
||||
val pipeIn = PipedInputStream()
|
||||
val pipeOut = PipedOutputStream(pipeIn)
|
||||
pool.execute {
|
||||
synchronized(this) {
|
||||
try {
|
||||
pipeOut.use {
|
||||
rar.extractFile(header, it)
|
||||
|
@ -1,16 +1,17 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.loader
|
||||
|
||||
import android.app.Application
|
||||
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 uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
/**
|
||||
* Loader used to load a chapter from a .zip or .cbz file.
|
||||
@ -22,85 +23,75 @@ internal class ZipPageLoader(
|
||||
// SY <--
|
||||
) : PageLoader() {
|
||||
|
||||
/**
|
||||
* The zip file to load pages from.
|
||||
*/
|
||||
// SY -->
|
||||
private var zip4j = ZipFile(file)
|
||||
private val context: Application by injectLazy()
|
||||
private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also {
|
||||
it.deleteRecursively()
|
||||
it.mkdirs()
|
||||
}
|
||||
|
||||
// SY -->
|
||||
init {
|
||||
if (zip4j.isEncrypted) {
|
||||
if (!CbzCrypto.checkCbzPassword(zip4j, CbzCrypto.getDecryptedPasswordCbz())) {
|
||||
val zip = ZipFile(file)
|
||||
if (zip.isEncrypted) {
|
||||
if (!CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())) {
|
||||
this.recycle()
|
||||
throw Exception(context.getString(R.string.wrong_cbz_archive_password))
|
||||
}
|
||||
unzipEncrypted(zip)
|
||||
} else {
|
||||
unzip(file)
|
||||
}
|
||||
}
|
||||
private fun unzip(file: File) {
|
||||
// SY <--
|
||||
ZipInputStream(FileInputStream(file)).use { zipInputStream ->
|
||||
generateSequence { zipInputStream.nextEntry }
|
||||
.filterNot { it.isDirectory }
|
||||
.forEach { entry ->
|
||||
File(tmpDir, entry.name).also { it.createNewFile() }
|
||||
.outputStream().use { pageOutputStream ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pageOutputStream.write(zipInputStream.readNBytes(entry.size.toInt()))
|
||||
} else {
|
||||
val buffer = ByteArray(2048)
|
||||
var len: Int
|
||||
while (
|
||||
zipInputStream.read(buffer, 0, buffer.size)
|
||||
.also { len = it } >= 0
|
||||
) {
|
||||
pageOutputStream.write(buffer, 0, len)
|
||||
}
|
||||
}
|
||||
pageOutputStream.flush()
|
||||
}
|
||||
zipInputStream.closeEntry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val zip: java.util.zip.ZipFile? =
|
||||
// SY -->
|
||||
private fun unzipEncrypted(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
|
||||
zip.charset = StandardCharsets.ISO_8859_1
|
||||
}
|
||||
zip.setPassword(CbzCrypto.getDecryptedPasswordCbz())
|
||||
|
||||
zip.fileHeaders.asSequence()
|
||||
.filterNot { !it.isDirectory }
|
||||
.forEach { entry ->
|
||||
zip.extractFile(entry, tmpDir.absolutePath)
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Recycles this loader and the open zip.
|
||||
*/
|
||||
override fun recycle() {
|
||||
super.recycle()
|
||||
// SY -->
|
||||
zip4j.close()
|
||||
zip?.close()
|
||||
// SY <--
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pages found on this zip archive ordered with a natural comparator.
|
||||
override var isLocal: Boolean = true
|
||||
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
// 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
|
||||
}
|
||||
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 <--
|
||||
}
|
||||
|
||||
override suspend fun loadPage(page: ReaderPage) {
|
||||
check(!isRecycled)
|
||||
return DirectoryPageLoader(tmpDir).getPages()
|
||||
}
|
||||
|
||||
override fun recycle() {
|
||||
super.recycle()
|
||||
zip.close()
|
||||
tmpDir.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
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(
|
||||
@ -10,9 +8,6 @@ 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 */
|
||||
|
@ -229,13 +229,7 @@ class PagerPageHolder(
|
||||
return splitInHalf(imageStream)
|
||||
}
|
||||
|
||||
val isDoublePage = ImageUtil.isWideImage(
|
||||
imageStream,
|
||||
// SY -->
|
||||
page.zip4jFile,
|
||||
page.zip4jEntry,
|
||||
// SY <--
|
||||
)
|
||||
val isDoublePage = ImageUtil.isWideImage(imageStream)
|
||||
if (!isDoublePage) {
|
||||
return imageStream
|
||||
}
|
||||
@ -260,13 +254,7 @@ class PagerPageHolder(
|
||||
if (imageStream2 == null) {
|
||||
return if (imageStream is BufferedInputStream &&
|
||||
!ImageUtil.isAnimatedAndSupported(imageStream) &&
|
||||
ImageUtil.isWideImage(
|
||||
imageStream,
|
||||
// SY -->
|
||||
page.zip4jFile,
|
||||
page.zip4jEntry,
|
||||
// SY <--
|
||||
) &&
|
||||
ImageUtil.isWideImage(imageStream) &&
|
||||
viewer.config.centerMarginType and PagerConfig.CenterMarginType.WIDE_PAGE_CENTER_MARGIN > 0 &&
|
||||
!viewer.config.imageCropBorders
|
||||
) {
|
||||
|
@ -213,13 +213,7 @@ class WebtoonPageHolder(
|
||||
|
||||
private fun process(imageStream: BufferedInputStream): InputStream {
|
||||
if (viewer.config.dualPageSplit) {
|
||||
val isDoublePage = ImageUtil.isWideImage(
|
||||
imageStream,
|
||||
// SY -->
|
||||
page?.zip4jFile,
|
||||
page?.zip4jEntry,
|
||||
// SY <--
|
||||
)
|
||||
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)
|
||||
@ -230,13 +224,7 @@ class WebtoonPageHolder(
|
||||
if (page is StencilPage) {
|
||||
return imageStream
|
||||
}
|
||||
val isStripSplitNeeded = ImageUtil.isStripSplitNeeded(
|
||||
imageStream,
|
||||
// SY -->
|
||||
page?.zip4jFile,
|
||||
page?.zip4jEntry,
|
||||
// SY <--
|
||||
)
|
||||
val isStripSplitNeeded = ImageUtil.isStripSplitNeeded(imageStream)
|
||||
if (isStripSplitNeeded) {
|
||||
return onStripSplit(imageStream)
|
||||
}
|
||||
@ -249,14 +237,7 @@ 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,
|
||||
// SY -->
|
||||
page.zip4jFile,
|
||||
page.zip4jEntry,
|
||||
// SY <--
|
||||
|
||||
).toMutableList()
|
||||
val splitData = ImageUtil.getSplitDataForStream(imageStream).toMutableList()
|
||||
val currentSplitData = splitData.removeFirst()
|
||||
val newPages = splitData.map {
|
||||
StencilPage(page) { ImageUtil.splitStrip(it, stream) }
|
||||
|
@ -26,8 +26,6 @@ import androidx.core.graphics.red
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import com.hippo.unifile.UniFile
|
||||
import logcat.LogPriority
|
||||
import net.lingala.zip4j.ZipFile
|
||||
import net.lingala.zip4j.model.FileHeader
|
||||
import tachiyomi.decoder.Format
|
||||
import tachiyomi.decoder.ImageDecoder
|
||||
import java.io.BufferedInputStream
|
||||
@ -127,17 +125,9 @@ object ImageUtil {
|
||||
*
|
||||
* @return true if the width is greater than the height
|
||||
*/
|
||||
fun isWideImage(
|
||||
imageStream: BufferedInputStream,
|
||||
zip4jFile: ZipFile? = null,
|
||||
zip4jEntry: FileHeader? = null,
|
||||
): Boolean {
|
||||
fun isWideImage(imageStream: BufferedInputStream): Boolean {
|
||||
val options = extractImageOptions(
|
||||
imageStream,
|
||||
// SY -->
|
||||
zip4jFile,
|
||||
zip4jEntry,
|
||||
// SY <--
|
||||
)
|
||||
return options.outWidth > options.outHeight
|
||||
}
|
||||
@ -263,19 +253,9 @@ object ImageUtil {
|
||||
*
|
||||
* @return true if the height:width ratio is greater than 3.
|
||||
*/
|
||||
private fun isTallImage(
|
||||
imageStream: InputStream,
|
||||
// SY -->
|
||||
zip4jFile: ZipFile? = null,
|
||||
zip4jEntry: FileHeader? = null,
|
||||
// SY <--
|
||||
): Boolean {
|
||||
private fun isTallImage(imageStream: InputStream): Boolean {
|
||||
val options = extractImageOptions(
|
||||
imageStream,
|
||||
// SY -->
|
||||
zip4jFile,
|
||||
zip4jEntry,
|
||||
// SY <--
|
||||
resetAfterExtraction = false,
|
||||
)
|
||||
|
||||
@ -285,23 +265,8 @@ object ImageUtil {
|
||||
/**
|
||||
* Splits tall images to improve performance of reader
|
||||
*/
|
||||
fun splitTallImage(
|
||||
tmpDir: UniFile,
|
||||
imageFile: UniFile,
|
||||
filenamePrefix: String,
|
||||
// SY -->
|
||||
zip4jFile: ZipFile? = null,
|
||||
zip4jEntry: FileHeader? = null,
|
||||
// SY <--
|
||||
): Boolean {
|
||||
if (isAnimatedAndSupported(imageFile.openInputStream()) || !isTallImage(
|
||||
imageFile.openInputStream(),
|
||||
// SY -->
|
||||
zip4jFile,
|
||||
zip4jEntry,
|
||||
// SY <--
|
||||
)
|
||||
) {
|
||||
fun splitTallImage(tmpDir: UniFile, imageFile: UniFile, filenamePrefix: String, ): Boolean {
|
||||
if (isAnimatedAndSupported(imageFile.openInputStream()) || !isTallImage(imageFile.openInputStream())) {
|
||||
return true
|
||||
}
|
||||
|
||||
@ -311,14 +276,7 @@ object ImageUtil {
|
||||
return false
|
||||
}
|
||||
|
||||
val options = extractImageOptions(
|
||||
imageFile.openInputStream(),
|
||||
// SY -->
|
||||
zip4jFile,
|
||||
zip4jEntry,
|
||||
// SY <--
|
||||
resetAfterExtraction = false,
|
||||
).apply {
|
||||
val options = extractImageOptions(imageFile.openInputStream(), resetAfterExtraction = false).apply {
|
||||
inJustDecodeBounds = false
|
||||
}
|
||||
|
||||
@ -364,22 +322,10 @@ object ImageUtil {
|
||||
* 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,
|
||||
// SY -->
|
||||
zip4jFile: ZipFile? = null,
|
||||
zip4jEntry: FileHeader? = null,
|
||||
// SY <--
|
||||
): Boolean {
|
||||
fun isStripSplitNeeded(imageStream: BufferedInputStream): Boolean {
|
||||
if (isAnimatedAndSupported(imageStream)) return false
|
||||
|
||||
val options = extractImageOptions(
|
||||
imageStream,
|
||||
// SY -->
|
||||
zip4jFile,
|
||||
zip4jEntry,
|
||||
// SY <--
|
||||
)
|
||||
val options = extractImageOptions(imageStream)
|
||||
val imageHeightIsBiggerThanWidth = options.outHeight > options.outWidth
|
||||
val imageHeightBiggerThanScreenHeight = options.outHeight > optimalImageHeight
|
||||
return imageHeightIsBiggerThanWidth && imageHeightBiggerThanScreenHeight
|
||||
@ -411,21 +357,8 @@ object ImageUtil {
|
||||
}
|
||||
}
|
||||
|
||||
fun getSplitDataForStream(
|
||||
imageStream: InputStream,
|
||||
// SY -->
|
||||
zip4jFile: ZipFile? = null,
|
||||
zip4jEntry: FileHeader? = null,
|
||||
// SY <--
|
||||
|
||||
): List<SplitData> {
|
||||
// SY -->
|
||||
return extractImageOptions(
|
||||
imageStream,
|
||||
zip4jFile,
|
||||
zip4jEntry,
|
||||
).splitData
|
||||
// <--
|
||||
fun getSplitDataForStream(imageStream: InputStream): List<SplitData> {
|
||||
return extractImageOptions(imageStream).splitData
|
||||
}
|
||||
|
||||
private val BitmapFactory.Options.splitData
|
||||
@ -690,17 +623,8 @@ object ImageUtil {
|
||||
*/
|
||||
private fun extractImageOptions(
|
||||
imageStream: InputStream,
|
||||
// SY -->
|
||||
zip4jFile: ZipFile? = null,
|
||||
zip4jEntry: FileHeader? = null,
|
||||
// SY <--
|
||||
resetAfterExtraction: Boolean = true,
|
||||
|
||||
): BitmapFactory.Options {
|
||||
// SY -->
|
||||
// zip4j does currently not support mark() and reset()
|
||||
if (zip4jFile != null && zip4jEntry != null) return extractImageOptionsZip4j(zip4jFile, zip4jEntry)
|
||||
// SY <--
|
||||
imageStream.mark(imageStream.available() + 1)
|
||||
|
||||
val imageBytes = imageStream.readBytes()
|
||||
@ -711,15 +635,6 @@ object ImageUtil {
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private fun extractImageOptionsZip4j(zip4jFile: ZipFile?, zip4jEntry: FileHeader?): BitmapFactory.Options {
|
||||
zip4jFile?.getInputStream(zip4jEntry).use { imageStream ->
|
||||
val imageBytes = imageStream?.readBytes()
|
||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
imageBytes?.size?.let { BitmapFactory.decodeByteArray(imageBytes, 0, it, options) }
|
||||
return options
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates random exif metadata used as padding to make
|
||||
* the size of files inside CBZ archives unique
|
||||
|
Loading…
x
Reference in New Issue
Block a user