From 6c70525e9d156bf3e27843a563223cc8cd39c024 Mon Sep 17 00:00:00 2001 From: Chopper <156493704+choppeh@users.noreply.github.com> Date: Thu, 21 Nov 2024 21:41:24 -0300 Subject: [PATCH] Use StaticLayout --- .../en/snowmtl/ComposedImageInterceptor.kt | 128 ++++++------------ .../extension/en/snowmtl/SnowmtlDto.kt | 42 +----- 2 files changed, 43 insertions(+), 127 deletions(-) diff --git a/src/en/snowmtl/src/eu/kanade/tachiyomi/extension/en/snowmtl/ComposedImageInterceptor.kt b/src/en/snowmtl/src/eu/kanade/tachiyomi/extension/en/snowmtl/ComposedImageInterceptor.kt index d7dfdf662..d104e2484 100644 --- a/src/en/snowmtl/src/eu/kanade/tachiyomi/extension/en/snowmtl/ComposedImageInterceptor.kt +++ b/src/en/snowmtl/src/eu/kanade/tachiyomi/extension/en/snowmtl/ComposedImageInterceptor.kt @@ -5,8 +5,11 @@ import android.graphics.BitmapFactory import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint -import android.graphics.Rect -import android.graphics.Typeface +import android.os.Build +import android.text.Layout +import android.text.StaticLayout +import android.text.TextPaint +import androidx.annotation.RequiresApi import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import okhttp3.Interceptor @@ -16,9 +19,10 @@ import okhttp3.ResponseBody.Companion.toResponseBody import uy.kohesive.injekt.injectLazy import java.io.ByteArrayOutputStream import java.io.IOException -import kotlin.math.absoluteValue +import kotlin.math.min // The Interceptor joins the captions and pages of the manga. +@RequiresApi(Build.VERSION_CODES.Q) class ComposedImageInterceptor( private val baseUrl: String, ) : Interceptor { @@ -53,83 +57,45 @@ class ComposedImageInterceptor( val canvas = Canvas(bitmap) val defaultTextSize = 22.sp // arbitrary - val paint = Paint().apply { - textAlign = Paint.Align.LEFT + val textPaint = TextPaint().apply { color = Color.BLACK style = Paint.Style.FILL_AND_STROKE textSize = defaultTextSize isAntiAlias = true - typeface = Typeface.SANS_SERIF } - val textMarginTop = 20 // arbitrary - val textMarginLeft = 40 // arbitrary - val spaceBetween = 2 // arbitrary - val defaultFontScale = 1f + val spacingMultiplier = 1f + val spacingAddition = 0f translation .filter { it.text.isNotBlank() } - .forEach { - var charWidth = paint.getCharWidth() - var charHeight = paint.getCharHeight() + .forEach { caption -> + val layout = StaticLayout.Builder.obtain(caption.text, 0, caption.text.length, textPaint, caption.width.toInt()).apply { + setAlignment(Layout.Alignment.ALIGN_CENTER) + setLineSpacing(spacingAddition, spacingMultiplier) + setIncludePad(false) + }.build() - var lines = it.breakLines(charWidth) - /* - Reduces the font according to the size of the line in the dialog box. (space between applied) - Ex. 1: - - Box: 10 lines - - Text: 9 lines - - Scale: 1 - Ex. 2: - - Box: 10 lines - - Text: 15 - - Scale: 0.6 (10/ (15 + 1)) // 1 extra line(arbitrary) + val fontHeight = textPaint.fontMetrics.let { it.bottom - it.top } + val dialogBoxLineCount = caption.height / fontHeight - Ex. 3: - - Box: 10 lines - - Text: 2 line - - Scale: 1 - */ - val dialogBoxLines = it.height / charHeight - val fontScale = when { - lines.size >= dialogBoxLines -> dialogBoxLines / (lines.size + 1) - else -> defaultFontScale - } - - // Use font scale in large dialogs - if (fontScale != defaultFontScale) { - paint.apply { - this.textSize = defaultTextSize * fontScale - } - // reprocessing break lines - charWidth = paint.getCharWidth() - charHeight = paint.getCharHeight() - lines = it.breakLines(charWidth) - } - - // Centers the text if it is smaller than half of the dialog box. - val isHalfTheBox = lines.size / dialogBoxLines < 0.5 - val initialY = when { - isHalfTheBox -> it.centerY - lines.size * charHeight / 2 - else -> it.y1.toFloat() - } - - // Invert color in black dialog box - paint.apply { - val pixelColor = bitmap.getPixel(it.centerX.toInt(), it.centerY.toInt()) + // Invert color in black dialog box and font scale. Change StaticLayout by reference + textPaint.apply { + val pixelColor = bitmap.getPixel(caption.centerX.toInt(), caption.centerY.toInt()) val inverseColor = (Color.WHITE - pixelColor) or Color.BLACK color = inverseColor + textSize = min(defaultTextSize * (dialogBoxLineCount / layout.lineCount), defaultTextSize) } - lines.forEachIndexed { index, line -> - // Centers the text on the X axis and positions it inside the dialog box - val x = (it.centerX - (line.length * charWidth / 2)).absoluteValue + textMarginLeft - - // Positions the text inside the dialog box on the Y axis - val y = (initialY + charHeight * index * spaceBetween).absoluteValue + textMarginTop - - canvas.drawText(line, 0, line.length, x, y, paint) + // Centers text in y for captions smaller than the dialog box + val y = when { + layout.lineCount < dialogBoxLineCount -> { + caption.centerY - layout.lineCount / 2f * fontHeight + } + else -> caption.y1 } + + canvas.draw(layout, caption.x1, y) } val output = ByteArrayOutputStream() @@ -153,33 +119,17 @@ class ComposedImageInterceptor( return json.decodeFromString(this) } - val Int.sp: Float get() = this * scaledDensity + private fun Canvas.draw(layout: StaticLayout, x: Float, y: Float) { + save() + translate(x, y) + layout.draw(this) + restore() + } + + private val Int.sp: Float get() = this * SCALED_DENSITY companion object { - const val scaledDensity = 1.5f // arbitrary + const val SCALED_DENSITY = 1.5f // arbitrary val mediaType = "image/png".toMediaType() } } - -/** - * Gets the pixel width of the font character, used to calculate the - * scale needed to apply to the font given the size of the dialog box - */ -fun Paint.getCharWidth(): Float { - val text = "A" // Just any character to get the size of the character box - val fontWidth = FloatArray(1) - getTextWidths(text.first().toString(), fontWidth) - return fontWidth.first() -} - -/** - * Gets the pixel height of the font character, used to calculate - * line breaks in the text, given the maximum amount supported - * in the dialog box. - */ -fun Paint.getCharHeight(): Float { - val text = "A" // Just any character to get the size of the character box - val bounds = Rect() - getTextBounds(text, 0, text.length, bounds) - return bounds.height().toFloat() -} diff --git a/src/en/snowmtl/src/eu/kanade/tachiyomi/extension/en/snowmtl/SnowmtlDto.kt b/src/en/snowmtl/src/eu/kanade/tachiyomi/extension/en/snowmtl/SnowmtlDto.kt index 258ec77ed..0ed847909 100644 --- a/src/en/snowmtl/src/eu/kanade/tachiyomi/extension/en/snowmtl/SnowmtlDto.kt +++ b/src/en/snowmtl/src/eu/kanade/tachiyomi/extension/en/snowmtl/SnowmtlDto.kt @@ -20,50 +20,16 @@ class PageDto( @Serializable class Translation( - val x1: Int, - val y1: Int, - val x2: Int, - val y2: Int, + val x1: Float, + val y1: Float, + val x2: Float, + val y2: Float, val text: String, ) { val width get() = x2 - x1 val height get() = y2 - y1 val centerY get() = (y2 + y1) / 2f val centerX get() = (x2 + x1) / 2f - - fun breakLines(charWidth: Float): List { - val diameter = width / charWidth - val radius = diameter / 2 - return breakTextIntoLines(text, diameter + radius) - } - - private fun breakTextIntoLines(text: String, maxLineLength: Float): List { - if (text.length <= maxLineLength) { - return listOf(text) - } - - val words = text.split(" ") - val lines = mutableListOf() - var currentLine = StringBuilder() - - for (word in words) { - if (currentLine.length + word.length <= maxLineLength) { - if (currentLine.isNotEmpty()) { - currentLine.append(" ") - } - currentLine.append(word) - } else { - lines.add(currentLine.toString().trim()) - currentLine = StringBuilder(word) - } - } - - if (currentLine.isNotEmpty()) { - lines.add(currentLine.toString()) - } - - return lines - } } private object TranslationsListSerializer :