diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index b2a40112b..59173243e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -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)) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt index 5374216ee..724283e56 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt @@ -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 { - 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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt index 8e322c014..50a378ad6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt @@ -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 { - // 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() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt index f8d205e65..0a6f2d414 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt @@ -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 */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index 51522945d..6f643562d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -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 ) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index 13570c945..b72a011da 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -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) } diff --git a/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt b/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt index fffbf8c1e..37e0e124d 100644 --- a/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt +++ b/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt @@ -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 { - // SY --> - return extractImageOptions( - imageStream, - zip4jFile, - zip4jEntry, - ).splitData - // <-- + fun getSplitDataForStream(imageStream: InputStream): List { + 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