From 1397b0a3341ce0fffd1ddcf1f3e4752b9c94ff41 Mon Sep 17 00:00:00 2001 From: HapeLee <63206378+HapeLee@users.noreply.github.com> Date: Tue, 30 Jun 2026 21:03:18 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=99=A8=E5=B9=B6=E6=94=AF=E6=8C=81=E4=B9=9D?= =?UTF-8?q?=E5=AE=AB=E6=A0=BC=E9=AB=98=E4=BA=AE=E8=83=8C=E6=99=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/ui/book/read/ReadBookRouteScreen.kt | 13 +- .../ui/book/read/page/entities/TextLine.kt | 155 +++++++++++++++++- .../book/read/sheet/HighlightRuleEditSheet.kt | 34 +++- 3 files changed, 185 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt index d65ca8834..59be73732 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt @@ -6,6 +6,7 @@ import android.view.View import android.widget.FrameLayout import android.widget.ImageView import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -165,14 +166,14 @@ fun ReadBookRouteScreen( } val readStyleImagePicker = rememberLauncherForActivityResult( - ActivityResultContracts.OpenDocument() + ActivityResultContracts.PickVisualMedia() ) { uri -> uri?.let { viewModel.onIntent(ReadBookIntent.ReadStyleImageSelected(it)) } } var pendingReadStyleImageIsNight by remember { mutableStateOf(false) } val readStyleImagePickerForMode = rememberLauncherForActivityResult( - ActivityResultContracts.OpenDocument() + ActivityResultContracts.PickVisualMedia() ) { uri -> uri?.let { viewModel.onIntent(ReadBookIntent.ReadStyleImageSelectedForMode(it, pendingReadStyleImageIsNight)) @@ -367,11 +368,15 @@ fun ReadBookRouteScreen( booksDirPicker.launch(null) } is ReadBookEffect.OpenReadStyleImagePicker -> { - readStyleImagePicker.launch(arrayOf("image/*")) + readStyleImagePicker.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) + ) } is ReadBookEffect.OpenReadStyleImagePickerForMode -> { pendingReadStyleImageIsNight = effect.isNight - readStyleImagePickerForMode.launch(arrayOf("image/*")) + readStyleImagePickerForMode.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) + ) } is ReadBookEffect.OpenReadStyleImport -> { readStyleImportPicker.launch( diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt index e4fd473f0..0faa4c77e 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt @@ -527,9 +527,12 @@ data class TextLine( val sh = rectHeight * scale val dx = startX + (rectWidth - sw) / 2f val dy = top + (rectHeight - sh) / 2f + val destination = android.graphics.RectF(dx, dy, dx + sw, dy + sh) canvas.save() canvas.clipRect(startX, top, endX, bottom) - canvas.drawBitmap(bitmap, null, android.graphics.RectF(dx, dy, dx + sw, dy + sh), paint) + if (!drawNinePatchBitmap(canvas, bitmap, destination, paint, bgImage)) { + canvas.drawBitmap(bitmap, null, destination, paint) + } canvas.restore() } 2 -> { @@ -565,6 +568,124 @@ data class TextLine( PaintPool.recycle(paint) } + private fun drawNinePatchBitmap( + canvas: Canvas, + bitmap: Bitmap, + destination: android.graphics.RectF, + paint: Paint, + path: String, + ): Boolean { + bitmap.ninePatchChunk?.takeIf(android.graphics.NinePatch::isNinePatchChunk)?.let { chunk -> + android.graphics.NinePatch(bitmap, chunk, null).draw( + canvas, + android.graphics.Rect( + destination.left.toInt(), + destination.top.toInt(), + destination.right.toInt(), + destination.bottom.toInt(), + ), + paint, + ) + return true + } + if (!isRawNinePatchPath(path) || bitmap.width < 3 || bitmap.height < 3) { + return false + } + + val horizontalStretch = findNinePatchStretchRange(bitmap, horizontal = true) ?: return false + val verticalStretch = findNinePatchStretchRange(bitmap, horizontal = false) ?: return false + val sourceX = intArrayOf( + 1, + horizontalStretch.first, + horizontalStretch.last + 1, + bitmap.width - 1, + ) + val sourceY = intArrayOf( + 1, + verticalStretch.first, + verticalStretch.last + 1, + bitmap.height - 1, + ) + + var leftWidth = (sourceX[1] - sourceX[0]).toFloat() + var rightWidth = (sourceX[3] - sourceX[2]).toFloat() + val fixedWidth = leftWidth + rightWidth + if (fixedWidth > destination.width() && fixedWidth > 0f) { + val ratio = destination.width() / fixedWidth + leftWidth *= ratio + rightWidth *= ratio + } + + var topHeight = (sourceY[1] - sourceY[0]).toFloat() + var bottomHeight = (sourceY[3] - sourceY[2]).toFloat() + val fixedHeight = topHeight + bottomHeight + if (fixedHeight > destination.height() && fixedHeight > 0f) { + val ratio = destination.height() / fixedHeight + topHeight *= ratio + bottomHeight *= ratio + } + + val destinationX = floatArrayOf( + destination.left, + destination.left + leftWidth, + destination.right - rightWidth, + destination.right, + ) + val destinationY = floatArrayOf( + destination.top, + destination.top + topHeight, + destination.bottom - bottomHeight, + destination.bottom, + ) + + for (row in 0..2) { + for (column in 0..2) { + if (sourceX[column] == sourceX[column + 1] || + sourceY[row] == sourceY[row + 1] || + destinationX[column] == destinationX[column + 1] || + destinationY[row] == destinationY[row + 1] + ) { + continue + } + canvas.drawBitmap( + bitmap, + android.graphics.Rect( + sourceX[column], + sourceY[row], + sourceX[column + 1], + sourceY[row + 1], + ), + android.graphics.RectF( + destinationX[column], + destinationY[row], + destinationX[column + 1], + destinationY[row + 1], + ), + paint, + ) + } + } + return true + } + + private fun findNinePatchStretchRange(bitmap: Bitmap, horizontal: Boolean): IntRange? { + val limit = if (horizontal) bitmap.width - 2 else bitmap.height - 2 + var first = -1 + var last = -1 + for (position in 1..limit) { + val color = if (horizontal) bitmap.getPixel(position, 0) else bitmap.getPixel(0, position) + val isMarker = android.graphics.Color.alpha(color) == 0xFF && + android.graphics.Color.red(color) == 0 && + android.graphics.Color.green(color) == 0 && + android.graphics.Color.blue(color) == 0 + if (isMarker) { + if (first < 0) first = position + last = position + } + } + return if (first >= 0) first..last else null + } + fun checkFastDraw(): Boolean { if (!ReadConfig.optimizeRender || exceed || !onlyTextColumn || textPage.isMsgPage) { return false @@ -664,25 +785,26 @@ data class TextLine( private fun loadBgBitmap(path: String): Bitmap? { return try { val ctx = appCtx + val sampleBitmap = !isRawNinePatchPath(path) if (path.startsWith("assets://")) { val assetPath = path.removePrefix("assets://") ctx.assets.open(assetPath).use { input -> - decodeSampledBitmap(input) + decodeSampledBitmap(input, sampleBitmap) } } else if (path.startsWith("content://")) { val uri = android.net.Uri.parse(path) ctx.contentResolver.openInputStream(uri)?.use { input -> - decodeSampledBitmap(input) + decodeSampledBitmap(input, sampleBitmap) } } else { val file = java.io.File(path) if (file.exists()) { - decodeSampledBitmapFile(path) + decodeSampledBitmapFile(path, sampleBitmap) } else { val assetPath = if (path.startsWith("bg/")) path else "bg/$path" runCatching { ctx.assets.open(assetPath).use { input -> - decodeSampledBitmap(input) + decodeSampledBitmap(input, sampleBitmap) } }.getOrNull() } @@ -692,25 +814,40 @@ data class TextLine( } } - private fun decodeSampledBitmap(input: java.io.InputStream): Bitmap? { + private fun decodeSampledBitmap( + input: java.io.InputStream, + sampleBitmap: Boolean, + ): Bitmap? { val buffered = if (input.markSupported()) input else java.io.BufferedInputStream(input) val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } buffered.mark(buffered.available()) BitmapFactory.decodeStream(buffered, null, options) - options.inSampleSize = calculateInSampleSize(options, bgSampleWidth, bgSampleHeight) + options.inSampleSize = if (sampleBitmap) { + calculateInSampleSize(options, bgSampleWidth, bgSampleHeight) + } else { + 1 + } options.inJustDecodeBounds = false buffered.reset() return BitmapFactory.decodeStream(buffered, null, options) } - private fun decodeSampledBitmapFile(path: String): Bitmap? { + private fun decodeSampledBitmapFile(path: String, sampleBitmap: Boolean): Bitmap? { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } BitmapFactory.decodeFile(path, options) - options.inSampleSize = calculateInSampleSize(options, bgSampleWidth, bgSampleHeight) + options.inSampleSize = if (sampleBitmap) { + calculateInSampleSize(options, bgSampleWidth, bgSampleHeight) + } else { + 1 + } options.inJustDecodeBounds = false return BitmapFactory.decodeFile(path, options) } + private fun isRawNinePatchPath(path: String): Boolean { + return path.substringBefore('?').substringBefore('#').endsWith(".9.png", ignoreCase = true) + } + private fun calculateInSampleSize( options: BitmapFactory.Options, reqWidth: Int, diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/HighlightRuleEditSheet.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/HighlightRuleEditSheet.kt index c5edd7d0e..b22546a99 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/HighlightRuleEditSheet.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/HighlightRuleEditSheet.kt @@ -2,7 +2,9 @@ package io.legado.app.ui.book.read.sheet import android.content.Intent import android.net.Uri +import android.provider.OpenableColumns import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Arrangement @@ -135,14 +137,34 @@ fun HighlightRuleEditSheet( // Validation var patternError by remember(show, rule) { mutableStateOf(null) } - // SAF image picker + // System photo picker, with the contract's built-in fallback on older devices. val imagePicker = rememberLauncherForActivityResult( - ActivityResultContracts.GetContent() + ActivityResultContracts.PickVisualMedia() ) { uri: Uri? -> if (uri != null) { val dir = File(appCtx.filesDir, "bg_images") if (!dir.exists()) dir.mkdirs() - val target = File(dir, "bg_${System.currentTimeMillis()}.jpg") + val displayName = context.contentResolver.query( + uri, + arrayOf(OpenableColumns.DISPLAY_NAME), + null, + null, + null, + )?.use { cursor -> + if (cursor.moveToFirst()) { + cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + .takeIf { it >= 0 } + ?.let(cursor::getString) + } else { + null + } + } + val suffix = when { + displayName?.endsWith(".9.png", ignoreCase = true) == true -> ".9.png" + displayName?.substringAfterLast('.', "").isNullOrBlank() -> ".img" + else -> ".${displayName.substringAfterLast('.')}" + } + val target = File(dir, "bg_${System.currentTimeMillis()}$suffix") context.contentResolver.openInputStream(uri)?.use { input -> target.outputStream().use { output -> input.copyTo(output) @@ -362,7 +384,11 @@ fun HighlightRuleEditSheet( TinyClickableSettingItem( title = stringResource(R.string.highlight_bg_image), description = bgImage.ifBlank { null }?.let { File(it).name }, - onClick = { imagePicker.launch("image/*") }, + onClick = { + imagePicker.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) + ) + }, ) } if (hasBgImage && bgImage.isNotBlank()) {