Use StaticLayout

This commit is contained in:
Chopper 2024-11-21 21:41:24 -03:00
parent 91f90ab5d5
commit 6c70525e9d
2 changed files with 43 additions and 127 deletions

View File

@ -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()
}

View File

@ -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<String> {
val diameter = width / charWidth
val radius = diameter / 2
return breakTextIntoLines(text, diameter + radius)
}
private fun breakTextIntoLines(text: String, maxLineLength: Float): List<String> {
if (text.length <= maxLineLength) {
return listOf(text)
}
val words = text.split(" ")
val lines = mutableListOf<String>()
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 :