From 0d55f319c993578198e0c8cf6991b97fb708c15f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <99822064+revonateB0T@users.noreply.github.com> Date: Sat, 14 Sep 2024 00:10:05 +0800 Subject: [PATCH] Fuckoff External Ads (#1617) * Add Rust implementation to detect QRCode from bitmap * Add QRCodeInterceptor Framework * Let image take hasQRCode info * Keep ReaderPage index internal * Move imageCache to use ReaderPage as key * Make PageLoader size & pages internal * Refine * Kill entry if QRCode * Rewrite request to respect index mapping * Debug: enable by default * Fix crash * Fixup * Fix identity * Revert to lruCache to avoid crash * Fixup * Clippy check for android only * Fixup * Resolve clippy * Switch * Apply switch * Restart pageloader restore pages * Cleanup * Cleanup Signed-off-by: Dude so hot * let it inference Signed-off-by: Dude so hot * Don't detect border for ads * Make coil interceptor runs in `Dispatchers.Default` * Revert "Restart pageloader restore pages" This reverts commit 8fd02fb7d9c677c119df72a767a6079f1899dc31. * Revert drop page changes * Use backGround color painter for ads * Only check ads for tags contains extraneous ads * Only check ads for head 5 images || tail 10 images * Move strip ADs switch to AdvancedScreen * Pick placeholder * Show Placeholder * Don't try to extract & decode Signed-off-by: Dude so hot * cleanup Signed-off-by: Dude so hot * Naming * Naming * Naming * Naming * For ArchivePageLoader as well * Hint * Fix flicker * Refine * Naming * QR -> Qr * Naming * QR -> Qr * Naming * Refine * Revert * Remove header check * Allow show ads * Adjust * Refine * Revert "Don't try to extract & decode" This reverts commit f35ef46f6ffc81485cf0f4c646ed79e524c29d32. Signed-off-by: Dude so hot * be strict Signed-off-by: Dude so hot * Just Extract, don't decode * Refine * Refine placeholder --------- Signed-off-by: Dude so hot Co-authored-by: Dude so hot Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .../com/hippo/ehviewer/EhApplication.kt | 6 + .../kotlin/com/hippo/ehviewer/Settings.kt | 1 + .../hippo/ehviewer/client/data/GalleryInfo.kt | 3 + .../ehviewer/coil/CropBorderInterceptor.kt | 15 +- .../coil/HardwareBitmapInterceptor.kt | 31 +- .../ehviewer/coil/MapExtraInfoInterceptor.kt | 29 ++ .../hippo/ehviewer/coil/MergeInterceptor.kt | 5 +- .../hippo/ehviewer/coil/QrCodeInterceptor.kt | 33 ++ .../ehviewer/gallery/ArchivePageLoader.kt | 10 +- .../hippo/ehviewer/gallery/EhPageLoader.kt | 4 - .../kotlin/com/hippo/ehviewer/image/Image.kt | 25 +- .../com/hippo/ehviewer/spider/SpiderQueen.kt | 15 +- .../com/hippo/ehviewer/ui/reader/PagerItem.kt | 25 + .../hippo/ehviewer/ui/reader/ReaderScreen.kt | 13 +- .../hippo/ehviewer/ui/reader/WebtoonViewer.kt | 1 - .../ehviewer/ui/settings/AdvancedScreen.kt | 31 ++ .../com/hippo/ehviewer/util/LauncherUtils.kt | 2 +- .../eu/kanade/tachiyomi/source/model/Page.kt | 1 + .../tachiyomi/ui/reader/ReaderPageSheet.kt | 3 + .../tachiyomi/ui/reader/loader/PageLoader.kt | 8 +- app/src/main/res/values-zh-rCN/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + app/src/main/rust/Cargo.lock | 480 +++++++++++++++++- app/src/main/rust/Cargo.toml | 7 +- app/src/main/rust/src/lib.rs | 51 +- 26 files changed, 730 insertions(+), 79 deletions(-) create mode 100644 app/src/main/kotlin/com/hippo/ehviewer/coil/MapExtraInfoInterceptor.kt create mode 100644 app/src/main/kotlin/com/hippo/ehviewer/coil/QrCodeInterceptor.kt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88a308299..923cb0253 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: - name: Clippy Check working-directory: ./app/src/main/rust - run: cargo clippy --all-targets --all-features -- -D warnings + run: cargo clippy --target aarch64-linux-android --all-features -- -D warnings - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 diff --git a/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt b/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt index 87378c0ce..514005013 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt @@ -42,7 +42,9 @@ import com.hippo.ehviewer.client.data.GalleryDetail import com.hippo.ehviewer.coil.CropBorderInterceptor import com.hippo.ehviewer.coil.DownloadThumbInterceptor import com.hippo.ehviewer.coil.HardwareBitmapInterceptor +import com.hippo.ehviewer.coil.MapExtraInfoInterceptor import com.hippo.ehviewer.coil.MergeInterceptor +import com.hippo.ehviewer.coil.QrCodeInterceptor import com.hippo.ehviewer.cronet.cronetHttpClient import com.hippo.ehviewer.dailycheck.checkDawn import com.hippo.ehviewer.dao.SearchDatabase @@ -68,6 +70,7 @@ import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.system.logcat import io.ktor.client.HttpClient import io.ktor.client.plugins.cookies.HttpCookies +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import logcat.AndroidLogcatLogger import logcat.LogPriority @@ -169,6 +172,7 @@ class EhApplication : } override fun newImageLoader(context: Context) = context.imageLoader { + interceptorCoroutineContext(Dispatchers.Default) components { serviceLoaderEnabled(false) add(KtorNetworkFetcherFactory { ktorClient }) @@ -178,6 +182,8 @@ class EhApplication : add(HardwareBitmapInterceptor) } add(CropBorderInterceptor) + add(QrCodeInterceptor) + add(MapExtraInfoInterceptor) if (isAtLeastP) { add(AnimatedImageDecoder.Factory(false)) } else { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt b/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt index b96f79f26..575d1629e 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt @@ -209,6 +209,7 @@ object Settings : DataStorePreferences(null) { val zoomStart = intPref("pref_zoom_start_key", 1) val showNavigationOverlayNewUser = boolPref("reader_navigation_overlay_new_user", true) val showNavigationOverlayOnStart = boolPref("reader_navigation_overlay_on_start", false) + val stripExtraneousAds = boolPref("strip_extraneous_ads", false) val userAgent get() = if (desktopSite) CHROME_USER_AGENT else CHROME_MOBILE_USER_AGENT diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/data/GalleryInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/data/GalleryInfo.kt index ba26e92f1..6b9c5a854 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/data/GalleryInfo.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/data/GalleryInfo.kt @@ -141,3 +141,6 @@ fun GalleryInfo.findBaseInfo(): BaseGalleryInfo = when (this) { } fun GalleryInfo.asGalleryDetail(): GalleryDetail? = this as? GalleryDetail + +val GalleryInfo.hasAds + get() = simpleTags?.any { "extraneous ads" in it } ?: false diff --git a/app/src/main/kotlin/com/hippo/ehviewer/coil/CropBorderInterceptor.kt b/app/src/main/kotlin/com/hippo/ehviewer/coil/CropBorderInterceptor.kt index b50832b76..34c68bfce 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/coil/CropBorderInterceptor.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/coil/CropBorderInterceptor.kt @@ -1,9 +1,7 @@ package com.hippo.ehviewer.coil import androidx.compose.ui.unit.IntRect -import coil3.BitmapImage import coil3.Extras -import coil3.Image import coil3.getExtra import coil3.intercept.Interceptor import coil3.intercept.Interceptor.Chain @@ -24,18 +22,13 @@ fun ImageRequest.Builder.maybeCropBorder(enable: Boolean) = apply { val ImageRequest.maybeCropBorder: Boolean get() = getExtra(maybeCropBorderKey) -class BitmapImageWithRect( - val rect: IntRect, - val image: BitmapImage, -) : Image by image - object CropBorderInterceptor : Interceptor { override suspend fun intercept(chain: Chain): ImageResult { val result = chain.proceed() if (chain.request.maybeCropBorder && result is SuccessResult) { val image = result.image - if (image is BitmapImage) { - val bitmap = image.bitmap + if (image is BitmapImageWithExtraInfo && !image.hasQrCode) { + val bitmap = image.image.bitmap val ratio = if (image.height > image.width) { image.height / image.width } else { @@ -47,8 +40,8 @@ object CropBorderInterceptor : Interceptor { val minHeight = image.height * CROP_THRESHOLD val rect = IntRect(array[0], array[1], array[2], array[3]) if (rect.width > minWidth && rect.height > minHeight) { - val new = BitmapImageWithRect(rect, image) - return result.copy(image = new, request = result.request, dataSource = result.dataSource) + val new = image.copy(rect = rect) + return result.copy(image = new) } } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/coil/HardwareBitmapInterceptor.kt b/app/src/main/kotlin/com/hippo/ehviewer/coil/HardwareBitmapInterceptor.kt index ddb637836..2ef143c3e 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/coil/HardwareBitmapInterceptor.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/coil/HardwareBitmapInterceptor.kt @@ -3,8 +3,6 @@ package com.hippo.ehviewer.coil import android.graphics.Bitmap import android.os.Build import androidx.annotation.RequiresApi -import androidx.compose.ui.unit.IntRect -import coil3.BitmapImage import coil3.Extras import coil3.asImage import coil3.getExtra @@ -30,25 +28,18 @@ object HardwareBitmapInterceptor : Interceptor { val result = chain.proceed() val request = result.request if (!request.allowHardware && result is SuccessResult) { - var rect: IntRect? = null val image = result.image - val bitmap = when (image) { - is BitmapImageWithRect -> image.image.also { rect = image.rect } - is BitmapImage -> image - else -> return result - }.bitmap - // Large hardware bitmaps have rendering issues (e.g. crash, empty) on some devices. - // This is not ideal but I haven't figured out how to probe the threshold. - // All we know is that it's less than the maximum texture size. - if (maxOf(bitmap.width, bitmap.height) <= request.hardwareThreshold) { - bitmap.copy(Bitmap.Config.HARDWARE, false)?.let { hwBitmap -> - bitmap.recycle() - val newImage = hwBitmap.asImage() - return result.copy( - image = rect?.let { BitmapImageWithRect(it, newImage as BitmapImage) } ?: newImage, - request = request, - dataSource = result.dataSource, - ) + if (image is BitmapImageWithExtraInfo) { + val bitmap = image.image.bitmap + // Large hardware bitmaps have rendering issues (e.g. crash, empty) on some devices. + // This is not ideal but I haven't figured out how to probe the threshold. + // All we know is that it's less than the maximum texture size. + if (maxOf(bitmap.width, bitmap.height) <= request.hardwareThreshold) { + bitmap.copy(Bitmap.Config.HARDWARE, false)?.let { hwBitmap -> + bitmap.recycle() + val newImage = hwBitmap.asImage() + return result.copy(image = image.copy(image = newImage)) + } } } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/coil/MapExtraInfoInterceptor.kt b/app/src/main/kotlin/com/hippo/ehviewer/coil/MapExtraInfoInterceptor.kt new file mode 100644 index 000000000..c01d9668e --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/coil/MapExtraInfoInterceptor.kt @@ -0,0 +1,29 @@ +package com.hippo.ehviewer.coil + +import androidx.compose.ui.unit.IntRect +import coil3.BitmapImage +import coil3.Image +import coil3.intercept.Interceptor +import coil3.request.ImageResult +import coil3.request.SuccessResult +import coil3.request.allowHardware + +data class BitmapImageWithExtraInfo( + val image: BitmapImage, + val rect: IntRect = IntRect(0, 0, image.width, image.height), + val hasQrCode: Boolean = false, +) : Image by image + +object MapExtraInfoInterceptor : Interceptor { + override suspend fun intercept(chain: Interceptor.Chain): ImageResult { + val result = chain.proceed() + val needMap = with(chain.request) { maybeCropBorder || detectQrCode || !allowHardware } + if (needMap && result is SuccessResult) { + val image = result.image + if (image is BitmapImage) { + return result.copy(image = BitmapImageWithExtraInfo(image)) + } + } + return result + } +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/coil/MergeInterceptor.kt b/app/src/main/kotlin/com/hippo/ehviewer/coil/MergeInterceptor.kt index 7b318b317..f2fb78b4b 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/coil/MergeInterceptor.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/coil/MergeInterceptor.kt @@ -13,7 +13,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withContext object MergeInterceptor : Interceptor { private val pendingContinuationMap: HashMap>> = hashMapOf() @@ -42,7 +41,7 @@ object MergeInterceptor : Interceptor { override suspend fun intercept(chain: Interceptor.Chain): ImageResult { val req = chain.request - val key = req.memoryCacheKey?.takeIf { it.isNormalPreviewKey || Settings.preloadThumbAggressively } ?: return withContext(req.interceptorCoroutineContext) { chain.proceed() } + val key = req.memoryCacheKey?.takeIf { it.isNormalPreviewKey || Settings.preloadThumbAggressively } ?: return chain.proceed() suspendCancellableCoroutine { continuation -> synchronized(pendingContinuationMapLock) { @@ -63,7 +62,7 @@ object MergeInterceptor : Interceptor { } try { - return withContext(req.interceptorCoroutineContext) { chain.proceed() }.apply { + return chain.proceed().apply { // Wake all pending continuations shared with the same memory key since we have written it to memory cache notifyScope.launch { synchronized(pendingContinuationMapLock) { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/coil/QrCodeInterceptor.kt b/app/src/main/kotlin/com/hippo/ehviewer/coil/QrCodeInterceptor.kt new file mode 100644 index 000000000..0e42ec20b --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/coil/QrCodeInterceptor.kt @@ -0,0 +1,33 @@ +package com.hippo.ehviewer.coil + +import coil3.Extras +import coil3.getExtra +import coil3.intercept.Interceptor +import coil3.request.ImageRequest +import coil3.request.ImageResult +import coil3.request.SuccessResult +import com.hippo.ehviewer.image.hasQrCode + +private val detectQrCodeKey = Extras.Key(default = false) + +fun ImageRequest.Builder.detectQrCode(enable: Boolean) = apply { + extras[detectQrCodeKey] = enable +} + +val ImageRequest.detectQrCode: Boolean + get() = getExtra(detectQrCodeKey) + +object QrCodeInterceptor : Interceptor { + override suspend fun intercept(chain: Interceptor.Chain): ImageResult { + val result = chain.proceed() + if (chain.request.detectQrCode && result is SuccessResult) { + val image = result.image + if (image is BitmapImageWithExtraInfo) { + val hasQrCode = hasQrCode(image.image.bitmap) + val new = image.copy(hasQrCode = hasQrCode) + return result.copy(image = new) + } + } + return result + } +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt b/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt index 04f0f7202..4b1ebcc8f 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt @@ -16,6 +16,7 @@ package com.hippo.ehviewer.gallery import android.os.ParcelFileDescriptor +import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings.archivePasswds import com.hippo.ehviewer.image.ByteBufferSource import com.hippo.ehviewer.image.Image @@ -52,6 +53,7 @@ class ArchivePageLoader( private val file: Path, gid: Long = 0, startPage: Int = 0, + private val hasAds: Boolean = false, passwdProvider: PasswdProvider? = null, ) : PageLoader2(gid, startPage) { private lateinit var pfd: ParcelFileDescriptor @@ -104,6 +106,8 @@ class ArchivePageLoader( } } + private fun mayBeAd(index: Int) = index > size - 10 + private suspend fun doRealWork(index: Int) { val buffer = extractToByteBuffer(index) ?: return notifyPageFailed(index, null) check(buffer.isDirect) @@ -120,7 +124,7 @@ class ArchivePageLoader( src.close() throw it } - val image = Image.decode(src) ?: return notifyPageFailed(index, null) + val image = Image.decode(src, hasAds && Settings.stripExtraneousAds.value && mayBeAd(index)) ?: return notifyPageFailed(index, null) runCatching { currentCoroutineContext().ensureActive() }.onFailure { @@ -142,10 +146,6 @@ class ArchivePageLoader( override val isReady: Boolean get() = size != 0 - override fun onCancelRequest(index: Int) { - mJobMap[index]?.cancel() - } - override val title by lazy { FileUtils.getNameFromFilename(file.name)!! } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/gallery/EhPageLoader.kt b/app/src/main/kotlin/com/hippo/ehviewer/gallery/EhPageLoader.kt index 9f16f2e06..f9ae912c9 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/gallery/EhPageLoader.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/gallery/EhPageLoader.kt @@ -66,10 +66,6 @@ class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) : override val isReady: Boolean get() = ::mSpiderQueen.isInitialized && mSpiderQueen.isReady - override fun onCancelRequest(index: Int) { - mSpiderQueen.cancelRequest(index) - } - override fun onGetPages(pages: Int) {} override fun onGet509(index: Int) {} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/image/Image.kt b/app/src/main/kotlin/com/hippo/ehviewer/image/Image.kt index 3a02a981f..394934573 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/image/Image.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/image/Image.kt @@ -33,7 +33,8 @@ import coil3.request.allowHardware import coil3.size.Dimension import coil3.size.Precision import com.hippo.ehviewer.Settings -import com.hippo.ehviewer.coil.BitmapImageWithRect +import com.hippo.ehviewer.coil.BitmapImageWithExtraInfo +import com.hippo.ehviewer.coil.detectQrCode import com.hippo.ehviewer.coil.hardwareThreshold import com.hippo.ehviewer.coil.maybeCropBorder import com.hippo.ehviewer.jni.isGif @@ -53,12 +54,16 @@ import splitties.init.appCtx class Image private constructor(image: CoilImage, private val src: ImageSource) { val size = image.size val rect = when (image) { - is BitmapImageWithRect -> image.rect - else -> image.run { IntRect(0, 0, width, height) } + is BitmapImageWithExtraInfo -> image.rect + else -> with(image) { IntRect(0, 0, width, height) } + } + val hasQrCode = when (image) { + is BitmapImageWithExtraInfo -> image.hasQrCode + else -> false } var innerImage: CoilImage? = when (image) { - is BitmapImageWithRect -> image.image + is BitmapImageWithExtraInfo -> image.image else -> image } @@ -76,7 +81,7 @@ class Image private constructor(image: CoilImage, private val src: ImageSource) companion object { private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 3 - private suspend fun Either.decodeCoil(): CoilImage { + private suspend fun Either.decodeCoil(checkExtraneousAds: Boolean): CoilImage { val req = appCtx.imageRequest { onLeft { data(it.source) } onRight { data(it.source.toUri()) } @@ -85,6 +90,7 @@ class Image private constructor(image: CoilImage, private val src: ImageSource) allowHardware(false) hardwareThreshold(Settings.hardwareBitmapThreshold) maybeCropBorder(Settings.cropBorder.value) + detectQrCode(checkExtraneousAds) memoryCachePolicy(CachePolicy.DISABLED) } return when (val result = appCtx.imageLoader.execute(req)) { @@ -93,7 +99,7 @@ class Image private constructor(image: CoilImage, private val src: ImageSource) } } - suspend fun decode(src: ImageSource): Image? { + suspend fun decode(src: ImageSource, checkExtraneousAds: Boolean = false): Image? { return runCatching { val image = when (src) { is PathSource -> { @@ -109,18 +115,18 @@ class Image private constructor(image: CoilImage, private val src: ImageSource) src.close() } } - return decode(source) + return decode(source, checkExtraneousAds) } } } - src.right().decodeCoil() + src.right().decodeCoil(checkExtraneousAds) } is ByteBufferSource -> { if (isAtLeastP && !isAtLeastU) { rewriteGifSource(src.source) } - src.left().decodeCoil() + src.left().decodeCoil(checkExtraneousAds) } } Image(image, src).apply { @@ -146,3 +152,4 @@ interface ByteBufferSource : ImageSource { } external fun detectBorder(bitmap: Bitmap): IntArray +external fun hasQrCode(bitmap: Bitmap): Boolean diff --git a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderQueen.kt b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderQueen.kt index a5fbc3efe..cc420097b 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderQueen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderQueen.kt @@ -28,6 +28,7 @@ import com.hippo.ehviewer.client.EhUrl.getGalleryDetailUrl import com.hippo.ehviewer.client.EhUrl.getGalleryMultiPageViewerUrl import com.hippo.ehviewer.client.EhUrl.referer import com.hippo.ehviewer.client.data.GalleryInfo +import com.hippo.ehviewer.client.data.hasAds import com.hippo.ehviewer.client.ehRequest import com.hippo.ehviewer.client.exception.QuotaExceededException import com.hippo.ehviewer.client.fetchUsingAsText @@ -284,10 +285,6 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS } } - fun cancelRequest(index: Int) { - mWorkerScope.cancelDecode(index) - } - fun preloadPages(pages: List, pair: Pair) { mWorkerScope.updateRAList(pages, pair) } @@ -512,10 +509,6 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS var isDownloadMode = false private set - fun cancelDecode(index: Int) { - decoder.cancel(index) - } - @Synchronized fun enterDownloadMode() { if (isDownloadMode) return @@ -749,9 +742,13 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS } } + private fun mayBeAd(index: Int) = index > size - 10 + + private val hasAds = galleryInfo.hasAds + private suspend fun doInJob(index: Int) { val src = mSpiderDen.getImageSource(index) ?: return - val image = Image.decode(src) + val image = Image.decode(src, hasAds && Settings.stripExtraneousAds.value && mayBeAd(index)) checkNotNull(image) runCatching { currentCoroutineContext().ensureActive() diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PagerItem.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PagerItem.kt index 0686ffffd..b0eefded5 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PagerItem.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PagerItem.kt @@ -26,11 +26,15 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import coil3.BitmapImage import coil3.DrawableImage +import coil3.compose.AsyncImagePainter +import coil3.compose.SubcomposeAsyncImage +import coil3.compose.SubcomposeAsyncImageContent import com.google.accompanist.drawablepainter.DrawablePainter import com.hippo.ehviewer.R import com.hippo.ehviewer.Settings import com.hippo.ehviewer.collectAsState import com.hippo.ehviewer.image.Image +import com.hippo.ehviewer.ui.settings.AdsPlaceholderFile import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.loader.PageLoader import eu.kanade.tachiyomi.ui.reader.model.ReaderPage @@ -95,6 +99,27 @@ fun PagerItem( }, ) } + Page.State.BLOCKED -> { + SubcomposeAsyncImage( + model = AdsPlaceholderFile, + contentDescription = null, + modifier = modifier.fillMaxSize(), + ) { + val placeholderState by painter.state.collectAsState() + if (placeholderState is AsyncImagePainter.State.Success) { + SubcomposeAsyncImageContent() + } else { + Box( + modifier = Modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT), + contentAlignment = Alignment.Center, + ) { + if (placeholderState is AsyncImagePainter.State.Error) { + Text(text = stringResource(id = R.string.blocked_image)) + } + } + } + } + } Page.State.ERROR -> { Box(modifier = modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT)) { Column( diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/ReaderScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/ReaderScreen.kt index 317335c8f..aa3c3da99 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/ReaderScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/ReaderScreen.kt @@ -54,6 +54,7 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.hippo.ehviewer.R import com.hippo.ehviewer.Settings import com.hippo.ehviewer.client.data.BaseGalleryInfo +import com.hippo.ehviewer.client.data.hasAds import com.hippo.ehviewer.collectAsState import com.hippo.ehviewer.download.DownloadManager import com.hippo.ehviewer.download.archiveFile @@ -70,6 +71,7 @@ import com.hippo.files.toOkioPath import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.PageIndicatorText import eu.kanade.tachiyomi.ui.reader.ReaderAppBars import eu.kanade.tachiyomi.ui.reader.ReaderContentOverlay @@ -79,6 +81,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.withIOContext import kotlin.coroutines.resume +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch @@ -165,8 +168,10 @@ fun AnimatedVisibilityScope.ReaderScreen(pageLoader: PageLoader2, info: BaseGall .collect { setOrientation(it) } } LaunchedEffect(pageLoader) { - Settings.cropBorder.changesFlow().collect { - pageLoader.restart() + with(Settings) { + merge(cropBorder.changesFlow(), stripExtraneousAds.changesFlow()).collect { + pageLoader.restart() + } } } val showSeekbar by Settings.showReaderSeekbar.collectAsState() @@ -214,6 +219,7 @@ fun AnimatedVisibilityScope.ReaderScreen(pageLoader: PageLoader2, info: BaseGall val onSelectPage = { page: ReaderPage -> if (Settings.readerLongTapAction.value) { launch { + val pageBlocked = page.status.value == Page.State.BLOCKED dialog { cont -> fun dispose() = cont.resume(Unit) val state = rememberModalBottomSheetState() @@ -230,6 +236,7 @@ fun AnimatedVisibilityScope.ReaderScreen(pageLoader: PageLoader2, info: BaseGall copy = { launchIO { with(pageLoader) { copy(page) } } }, save = { launchIO { with(pageLoader) { save(page) } } }, saveTo = { launchIO { with(pageLoader) { saveTo(page) } } }, + showAds = { page.status.value = Page.State.READY }.takeIf { pageBlocked }, dismiss = { launch { state.hide().also { dispose() } } }, ) } @@ -345,7 +352,7 @@ private suspend fun preparePageLoader(args: ReaderScreenArgs) = when (args) { val page = args.page val archive = withIOContext { DownloadManager.getDownloadInfo(info.gid)?.archiveFile } if (archive != null) { - ArchivePageLoader(archive, info.gid, page) + ArchivePageLoader(archive, info.gid, page, info.hasAds) } else { EhPageLoader(info, page) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/WebtoonViewer.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/WebtoonViewer.kt index 7545a8272..27ee62189 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/WebtoonViewer.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/WebtoonViewer.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.unit.dp import com.hippo.ehviewer.Settings import com.hippo.ehviewer.collectAsState import com.hippo.ehviewer.gallery.PageLoader2 -import com.hippo.ehviewer.ui.main.plus import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.viewer.NavigationRegions import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AdvancedScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AdvancedScreen.kt index 02842e8e1..f2ea49eb0 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AdvancedScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AdvancedScreen.kt @@ -5,7 +5,10 @@ import android.content.Intent import android.net.Uri import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS import android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS +import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageOnly +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState @@ -39,6 +42,7 @@ import com.hippo.ehviewer.client.EhEngine import com.hippo.ehviewer.client.data.FavListUrlBuilder import com.hippo.ehviewer.collectAsState import com.hippo.ehviewer.ui.tools.observed +import com.hippo.ehviewer.ui.tools.rememberedAccessor import com.hippo.ehviewer.util.AppConfig import com.hippo.ehviewer.util.Crash import com.hippo.ehviewer.util.ReadableTime @@ -47,13 +51,16 @@ import com.hippo.ehviewer.util.getAppLanguage import com.hippo.ehviewer.util.getLanguages import com.hippo.ehviewer.util.isAtLeastO import com.hippo.ehviewer.util.isAtLeastV +import com.hippo.ehviewer.util.sendTo import com.hippo.ehviewer.util.setAppLanguage +import com.hippo.files.delete import com.hippo.files.toOkioPath import com.jamal.composeprefs3.ui.prefs.DropDownPref import com.jamal.composeprefs3.ui.prefs.SwitchPref import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.system.logcat import java.io.File import java.util.zip.ZipEntry @@ -62,6 +69,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import moe.tarsin.coroutines.runSuspendCatching +import okio.Path.Companion.toOkioPath +import splitties.init.appCtx @Destination @Composable @@ -91,6 +100,26 @@ fun AdvancedScreen(navigator: DestinationsNavigator) { summary = stringResource(id = R.string.settings_advanced_save_parse_error_body_summary), value = Settings::saveParseErrorBody, ) + val stripAds = Settings.stripExtraneousAds.asMutableState() + SwitchPreference( + title = stringResource(id = R.string.settings_strip_extraneous_ads), + value = stripAds.rememberedAccessor, + ) + AnimatedVisibility(visible = stripAds.value) { + LauncherPreference( + title = stringResource(id = R.string.settings_ads_placeholder), + contract = ActivityResultContracts.PickVisualMedia(), + key = PickVisualMediaRequest(mediaType = ImageOnly), + ) { uri -> + withIOContext { + if (uri != null) { + uri.toOkioPath() sendTo AdsPlaceholderFile + } else { + AdsPlaceholderFile.delete() + } + } + } + } SwitchPreference( title = stringResource(id = R.string.settings_advanced_save_crash_log), summary = stringResource(id = R.string.settings_advanced_save_crash_log_summary), @@ -273,3 +302,5 @@ fun AdvancedScreen(navigator: DestinationsNavigator) { } } } + +val AdsPlaceholderFile = appCtx.filesDir.toOkioPath() / "AdsPlaceholder" diff --git a/app/src/main/kotlin/com/hippo/ehviewer/util/LauncherUtils.kt b/app/src/main/kotlin/com/hippo/ehviewer/util/LauncherUtils.kt index e6a725e29..88fa64b5b 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/util/LauncherUtils.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/util/LauncherUtils.kt @@ -74,7 +74,7 @@ suspend fun Context.requestPermission(key: String): Boolean { return awaitActivityResult(ActivityResultContracts.RequestPermission(), key) } -suspend fun Context.pickVisualMedia(type: VisualMediaType): Uri? = awaitActivityResult(ActivityResultContracts.PickVisualMedia(), PickVisualMediaRequest.Builder().setMediaType(type).build()) +suspend fun Context.pickVisualMedia(type: VisualMediaType): Uri? = awaitActivityResult(ActivityResultContracts.PickVisualMedia(), PickVisualMediaRequest(mediaType = type)) @RequiresApi(Build.VERSION_CODES.O) suspend fun Context.requestInstallPermission(): Boolean { diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/source/model/Page.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/source/model/Page.kt index 96b6d3db3..b610dab48 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/source/model/Page.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/source/model/Page.kt @@ -26,6 +26,7 @@ open class Page(val index: Int) { LOAD_PAGE, DOWNLOAD_IMAGE, READY, + BLOCKED, ERROR, } } diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt index c70a8e8b3..d8cdebf07 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt @@ -18,6 +18,7 @@ import androidx.compose.material.icons.filled.FileCopy import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Save import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.filled.Visibility import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -37,6 +38,7 @@ fun ReaderPageSheetMeta( copy: () -> Unit, save: () -> Unit, saveTo: () -> Unit, + showAds: (() -> Unit)?, dismiss: () -> Unit, ) { @Composable @@ -52,6 +54,7 @@ fun ReaderPageSheetMeta( modifier = Modifier.fillMaxSize() // Workaround for https://issuetracker.google.com/341594885 .verticalScroll(rememberScrollState()).navigationBarsPadding(), ) { + showAds?.let { Item(icon = Icons.Default.Visibility, text = R.string.show_blocked_image, onClick = it) } Item(icon = Icons.Default.Refresh, text = R.string.refresh, onClick = retry) Item(icon = Icons.Default.Refresh, text = R.string.refresh_original, onClick = retryOrigin) Item(icon = Icons.Default.Share, text = R.string.action_share, onClick = share) diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt index 09d75d864..bd35c09ce 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt @@ -105,12 +105,6 @@ abstract class PageLoader { protected abstract fun onForceRequest(index: Int, orgImg: Boolean) - fun cancelRequest(index: Int) { - onCancelRequest(index) - } - - protected abstract fun onCancelRequest(index: Int) - fun notifyPageWait(index: Int) { pages[index].reset() } @@ -125,7 +119,7 @@ abstract class PageLoader { cache.put(index, image) } pages[index].image = image - pages[index].status.value = Page.State.READY + pages[index].status.value = if (image.hasQrCode) Page.State.BLOCKED else Page.State.READY } fun notifyPageFailed(index: Int, error: String?) { diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index c148f306d..06385fefe 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -321,6 +321,8 @@ 快 慢 翻转音量键 + 移除外部广告 + 选择替换外部广告的图片 下载 下载路径 无法获取下载路径 @@ -606,6 +608,8 @@ 总是关闭 总是开启 裁剪边缘 + 已屏蔽的图片 + 显示已屏蔽的图片 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0d8ebf940..2764f2f57 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -337,6 +337,8 @@ Fast Slow Reverse volume key + Remove extraneous ads + Pick placeholder for removed ads Download Download location Can\'t get download location @@ -624,6 +626,8 @@ Follow system Always off Always on + Blocked image + Show blocked image diff --git a/app/src/main/rust/Cargo.lock b/app/src/main/rust/Cargo.lock index 412e8a84b..904d6bcd4 100644 --- a/app/src/main/rust/Cargo.lock +++ b/app/src/main/rust/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "android_log-sys" version = "0.3.1" @@ -25,6 +37,55 @@ version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8" +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bardecoder" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757eeab9757d5ddd88aa1420449f3ce92631339b7f5b3785808da7a148de35d9" +dependencies = [ + "anyhow", + "image", + "log", + "newtype_derive", + "thiserror", +] + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bytemuck" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.7.1" @@ -43,6 +104,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.7" @@ -53,15 +120,58 @@ dependencies = [ "memchr", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "ehviewer_rust" version = "0.0.0" dependencies = [ "android_logger", "anyhow", + "bardecoder", + "image", "jni", "jni_fn", "log", + "ndk", "quick-xml", "regex-lite", "serde", @@ -69,6 +179,12 @@ dependencies = [ "tl", ] +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "env_filter" version = "0.1.2" @@ -78,12 +194,116 @@ dependencies = [ "log", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "exr" +version = "1.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +dependencies = [ + "bit_field", + "flume", + "half 2.4.1", + "lebe", + "miniz_oxide 0.7.4", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.0", +] + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "spin", +] + +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "half" version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-traits", + "png", + "qoi", + "tiff", +] + +[[package]] +name = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "jni" version = "0.21.1" @@ -117,6 +337,31 @@ dependencies = [ "syn", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +dependencies = [ + "rayon", +] + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.22" @@ -129,6 +374,110 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.6.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "newtype_derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac8cd24d9f185bb7223958d8c1ff7a961b74b1953fd05dba7cc568a63b3861ec" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.7.4", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -138,6 +487,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + [[package]] name = "quick-xml" version = "0.36.1" @@ -156,12 +514,47 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "regex-lite" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" +[[package]] +name = "rustc_version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084" +dependencies = [ + "semver", +] + [[package]] name = "same-file" version = "1.0.6" @@ -171,6 +564,18 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4f410fedcf71af0345d7607d246e7ad15faaadd49d240ee3b24e5dc21a820ac" + [[package]] name = "serde" version = "1.0.210" @@ -186,7 +591,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" dependencies = [ - "half", + "half 1.8.3", "serde", ] @@ -201,6 +606,27 @@ dependencies = [ "syn", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "syn" version = "2.0.77" @@ -232,12 +658,40 @@ dependencies = [ "syn", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "tl" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b130bd8a58c163224b44e217b4239ca7b927d82bf6cc2fea1fc561d15056e3f7" +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -254,6 +708,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "winapi-util" version = "0.1.9" @@ -401,3 +861,21 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/app/src/main/rust/Cargo.toml b/app/src/main/rust/Cargo.toml index ca8ac1cbc..c88d36c01 100644 --- a/app/src/main/rust/Cargo.toml +++ b/app/src/main/rust/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -regex-lite = { version = "0.1" } +regex-lite = "0.1" jni = "0.21" jni_fn = "0.1" quick-xml = { default-features = false, version = "0.36" } @@ -16,7 +16,10 @@ log = "0.4" android_logger = { default-features = false, version = "0.14" } serde_cbor = "0.11" serde = { version = "1.0", features = ["derive"] } -anyhow = { version = "1.0" } +anyhow = "1.0" +bardecoder = "0.5" +image = "0.24" +ndk = { version = "0.9", features = ["bitmap"] } [profile.release] panic = "abort" diff --git a/app/src/main/rust/src/lib.rs b/app/src/main/rust/src/lib.rs index f590a48bc..c768dd5ff 100644 --- a/app/src/main/rust/src/lib.rs +++ b/app/src/main/rust/src/lib.rs @@ -2,13 +2,21 @@ mod parser; use android_logger::Config; use anyhow::{anyhow, ensure, Result}; -use jni::objects::JByteBuffer; -use jni::sys::{jint, jobject, JavaVM, JNI_VERSION_1_6}; +use bardecoder::decode::Decode; +use bardecoder::default_builder; +use bardecoder::extract::QRExtractor; +use bardecoder::util::qr::{QRData, QRError}; +use image::ImageBuffer; +use jni::objects::{JByteBuffer, JClass}; +use jni::sys::{jboolean, jint, jobject, JavaVM, JNI_VERSION_1_6}; use jni::JNIEnv; +use jni_fn::jni_fn; use log::LevelFilter; +use ndk::bitmap::Bitmap; use serde::Serialize; use std::ffi::c_void; use std::io::Cursor; +use std::ptr::slice_from_raw_parts; use std::ptr::slice_from_raw_parts_mut; use std::str::from_utf8_unchecked; use tl::ParserOptions; @@ -64,6 +72,12 @@ trait ThrowingHasDefault { fn default() -> Self; } +impl ThrowingHasDefault for jboolean { + fn default() -> Self { + 0 as jboolean + } +} + impl ThrowingHasDefault for jobject { fn default() -> Self { 0 as jobject @@ -133,6 +147,39 @@ fn query_childs_first_match_attr<'a>( get_node_handle_attr(&iter.next()?, parser, attr) } +fn get_bitmap_handle(env: &mut JNIEnv, bitmap: jobject) -> Bitmap { + unsafe { Bitmap::from_jni(env.get_raw(), bitmap) } +} + +#[allow(dead_code)] +struct Nothing {} + +impl Decode for Nothing { + fn decode(&self, result: Result) -> Result { + result.map(|_| String::new()) + } +} + +#[no_mangle] +#[allow(non_snake_case)] +#[jni_fn("com.hippo.ehviewer.image.ImageKt")] +pub fn hasQrCode(mut env: JNIEnv, _class: JClass, object: jobject) -> jboolean { + let handle = get_bitmap_handle(&mut env, object); + jni_throwing(&mut env, |_| { + let bitmap_info = handle.info()?; + let (width, height) = (bitmap_info.width(), bitmap_info.height()); + let ptr = handle.lock_pixels()? as *const u8; + let buffer = unsafe { &*slice_from_raw_parts(ptr, (width * height * 4) as usize) }; + let mut decoder = default_builder(); + decoder.qr(Box::new(QRExtractor {}), Box::new(Nothing {})); + let image = ImageBuffer::from_raw(width, height, buffer); + let result = image.map(|img| decoder.build().decode(&img)); + handle.unlock_pixels()?; + let vec = result.ok_or(anyhow!("Internal Error!"))?; + Ok(vec.iter().any(|i| i.is_ok()) as jboolean) + }) +} + #[no_mangle] pub extern "system" fn JNI_OnLoad(_: JavaVM, _: *mut c_void) -> jint { android_logger::init_once(Config::default().with_max_level(LevelFilter::Debug));