Zaimanhua: Improve comment rendering (#10340)

- Better text layout and spacing for comments.
- Ensure the generated image is not too short, to avoid being displayed as a double-page spread.
This commit is contained in:
zhongfly 2025-09-01 17:09:29 +08:00 committed by Draff
parent 314b8f3848
commit 14b5edc771
Signed by: Draff
GPG Key ID: E8A89F3211677653
2 changed files with 76 additions and 31 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Zaimanhua' extName = 'Zaimanhua'
extClass = '.Zaimanhua' extClass = '.Zaimanhua'
extVersionCode = 11 extVersionCode = 12
isNsfw = false isNsfw = false
} }

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.extension.zh.zaimanhua
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface
import android.text.Layout import android.text.Layout
import android.text.StaticLayout import android.text.StaticLayout
import android.text.TextPaint import android.text.TextPaint
@ -13,21 +14,26 @@ import okhttp3.Response
import okhttp3.ResponseBody.Companion.asResponseBody import okhttp3.ResponseBody.Companion.asResponseBody
import okio.Buffer import okio.Buffer
fun parseChapterComments(response: Response, count: Int): List<String> { fun parseChapterComments(response: Response): List<String> {
val result = response.parseAs<ResponseDto<CommentDataDto>>() val result = response.parseAs<ResponseDto<CommentDataDto>>()
val comments = result.data.toCommentList() val comments = result.data.toCommentList()
return if (result.errmsg.isNotBlank()) { return if (result.errmsg.isNotBlank()) {
throw Exception(result.errmsg) throw Exception(result.errmsg)
} else { } else {
comments.take(count) comments
} }
} }
object CommentsInterceptor : Interceptor { object CommentsInterceptor : Interceptor {
private const val MAX_HEIGHT = 1920 private const val MAX_HEIGHT = 1920
private const val WIDTH = 1080 private const val WIDTH = 1080
private const val UNIT = 32 private const val X_PADDING: Float = 50f
private const val UNIT_F = UNIT.toFloat() private const val Y_PADDING: Float = 25f
private const val SPACING_MULT: Float = 1f
private const val SPACING_ADD: Float = 0f
private const val HEADING_FONT_SIZE: Float = 36f
private const val BODY_FONT_SIZE: Float = 30f
private const val SPACING: Float = BODY_FONT_SIZE * SPACING_MULT + SPACING_ADD
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request() val request = chain.request()
@ -35,46 +41,85 @@ object CommentsInterceptor : Interceptor {
if (request.tag(String::class) != COMMENTS_FLAG) { if (request.tag(String::class) != COMMENTS_FLAG) {
return response return response
} }
val comments = parseChapterComments(response, MAX_HEIGHT / (UNIT * 2) - 1).toMutableList()
comments.add(0, "章末吐槽:")
val paint = TextPaint().apply { val paintHeading = TextPaint().apply {
color = Color.BLACK color = Color.BLACK
textSize = UNIT_F textSize = HEADING_FONT_SIZE
typeface = Typeface.DEFAULT_BOLD
isAntiAlias = true isAntiAlias = true
} }
var height = UNIT @Suppress("DEPRECATION")
val layouts = comments.map { val heading = StaticLayout(
"章末吐槽:",
paintHeading,
(WIDTH - 2 * X_PADDING).toInt(),
Layout.Alignment.ALIGN_NORMAL,
SPACING_MULT,
SPACING_ADD,
true,
)
val comments = parseChapterComments(response).toMutableList()
val paintBody = TextPaint().apply {
color = Color.BLACK
textSize = BODY_FONT_SIZE
typeface = Typeface.DEFAULT
isAntiAlias = true
}
var currentHeight = Y_PADDING + heading.height
val bodyLayouts = mutableListOf<StaticLayout>()
for (comment in comments) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
StaticLayout(it, paint, WIDTH - 2 * UNIT, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false) val layout = StaticLayout(
}.takeWhile { comment,
val lineHeight = it.height + UNIT paintBody,
if (height + lineHeight <= MAX_HEIGHT) { (WIDTH - 2 * X_PADDING).toInt(),
height += lineHeight Layout.Alignment.ALIGN_NORMAL,
true SPACING_MULT,
} else { SPACING_ADD,
false true,
)
val lineHeight = SPACING + layout.height
// If adding this comment doesn't exceed the max height, add it.
// If it does exceed and it's a single line, stop adding more comments.
// Otherwise, if it's multi-line, just skip it and try the next comment.
if (currentHeight + lineHeight <= MAX_HEIGHT) {
bodyLayouts.add(layout)
currentHeight += lineHeight
} else if (layout.lineCount == 1) {
break
} }
} }
val bitmap = Bitmap.createBitmap(WIDTH, height, Bitmap.Config.ARGB_8888) // The bitmap height must be no more than MAX_HEIGHT
bitmap.eraseColor(Color.WHITE) // and no less than its width to prevent automatic double-page splitting.
val canvas = Canvas(bitmap) val bitmapHeight = (currentHeight + Y_PADDING).toInt().coerceIn(WIDTH, MAX_HEIGHT)
var y = UNIT val bitmap = Bitmap.createBitmap(WIDTH, bitmapHeight, Bitmap.Config.ARGB_8888)
for (layout in layouts) { Canvas(bitmap).apply {
canvas.save() drawColor(Color.WHITE)
canvas.translate(UNIT_F, y.toFloat()) heading.draw(this, X_PADDING, Y_PADDING)
layout.draw(canvas) var y = Y_PADDING + heading.height + SPACING
canvas.restore() for (layout in bodyLayouts) {
y += layout.height + UNIT layout.draw(this, X_PADDING, y)
y += layout.height + SPACING
}
} }
val responseBody = Buffer().run { val responseBody = Buffer().run {
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream()) bitmap.compress(Bitmap.CompressFormat.PNG, 0, outputStream())
asResponseBody("image/jpeg".toMediaType()) asResponseBody("image/png".toMediaType())
} }
return response.newBuilder().body(responseBody).build() return response.newBuilder().body(responseBody).build()
} }
@Suppress("SameParameterValue")
private fun StaticLayout.draw(canvas: Canvas, x: Float, y: Float) {
canvas.save()
canvas.translate(x, y)
this.draw(canvas)
canvas.restore()
}
} }