Skip to content

Commit fa4dbbd

Browse files
committed
Refine OverlayView; Remove OcrPostProcessorTest
1 parent 6068b26 commit fa4dbbd

5 files changed

Lines changed: 96 additions & 186 deletions

File tree

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ android {
1313
applicationId = "com.longipinnatus.screentrans"
1414
minSdk = 26
1515
targetSdk = 37
16-
versionCode = 26050201
17-
versionName = "0.1.1"
16+
versionCode = 26050701
17+
versionName = "0.1.2"
1818

1919
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2020
versionNameSuffix = "-alpha"

app/src/main/kotlin/com/longipinnatus/screentrans/LogActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ class LogActivity : ComponentActivity() {
249249
val clipboard = context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
250250
val clip = ClipData.newPlainText("log_data", text)
251251
clipboard.setPrimaryClip(clip)
252-
Toast.makeText(context, resources.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT).show()
252+
Toast.makeText(context, resources.getString(R.string.copied_to_clipboard, label), Toast.LENGTH_SHORT).show()
253253
}
254254
)
255255
}

app/src/main/kotlin/com/longipinnatus/screentrans/OcrEntities.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,6 @@ data class TextBlock(
7575
var backgroundColor: Int? = null,
7676
var colorWeight: Int = 0
7777
) : OcrEntity {
78-
val firstLineBounds: Rect get() = lines.firstOrNull()?.bounds ?: bounds
79-
val lastLineBounds: Rect get() = lines.lastOrNull()?.bounds ?: bounds
80-
8178
fun offset(dx: Int, dy: Int) {
8279
bounds.offset(dx, dy)
8380
lines.forEach { it.offset(dx, dy) }

app/src/main/kotlin/com/longipinnatus/screentrans/OverlayView.kt

Lines changed: 93 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import android.graphics.Color
66
import android.graphics.Paint
77
import android.graphics.RectF
88
import android.graphics.Typeface
9+
import android.os.Build
910
import android.text.Layout
1011
import android.text.StaticLayout
1112
import android.text.TextPaint
@@ -41,14 +42,14 @@ class OverlayView @JvmOverloads constructor(
4142
null
4243
}
4344
} ?: Typeface.DEFAULT
44-
45+
4546
val style = when {
4647
settings.overlayFontBold && settings.overlayFontItalic -> Typeface.BOLD_ITALIC
4748
settings.overlayFontBold -> Typeface.BOLD
4849
settings.overlayFontItalic -> Typeface.ITALIC
4950
else -> Typeface.NORMAL
5051
}
51-
52+
5253
typeface = Typeface.create(baseTypeface, style)
5354
}
5455

@@ -86,17 +87,17 @@ class OverlayView @JvmOverloads constructor(
8687

8788
fun updateStreamingDeadline(incrementalTextLength: Int) {
8889
if (incrementalTextLength <= 0) return
89-
90+
9091
val currentTime = System.currentTimeMillis()
9192
streamingDeadline = max(streamingDeadline, currentTime) + (incrementalTextLength.toLong() * settings.durationPerWordMs)
9293
}
9394

9495
override fun onDraw(canvas: Canvas) {
9596
super.onDraw(canvas)
96-
97+
9798
// Get the current view's absolute position offset on the screen
9899
getLocationOnScreen(locationOnScreen)
99-
100+
100101
val screenWidth = resources.displayMetrics.widthPixels
101102
val screenHeight = resources.displayMetrics.heightPixels
102103

@@ -126,13 +127,15 @@ class OverlayView @JvmOverloads constructor(
126127
} else {
127128
drawHorizontalBlock(canvas, block, text, localLeft, localTop)
128129
}
130+
}
129131

130-
if (settings.showBoxes) {
132+
if (settings.showBoxes) {
133+
textBlocks.forEach { block ->
131134
debugRect.set(
132-
bounds.left.toFloat() - locationOnScreen[0],
133-
bounds.top.toFloat() - locationOnScreen[1],
134-
bounds.right.toFloat() - locationOnScreen[0],
135-
bounds.bottom.toFloat() - locationOnScreen[1]
135+
block.bounds.left.toFloat() - locationOnScreen[0],
136+
block.bounds.top.toFloat() - locationOnScreen[1],
137+
block.bounds.right.toFloat() - locationOnScreen[0],
138+
block.bounds.bottom.toFloat() - locationOnScreen[1]
136139
)
137140
canvas.drawRect(debugRect, boxPaint)
138141
}
@@ -171,31 +174,60 @@ class OverlayView @JvmOverloads constructor(
171174
bgPaint.color = Color.argb(alpha, Color.red(it), Color.green(it), Color.blue(it))
172175
}
173176
}
174-
175-
// Prioritize original line height for font size
176-
val originalLineHeight = block.firstLineBounds.height().toFloat()
177-
textPaint.textSize = originalLineHeight.coerceIn(12f, 120f)
178-
179-
var layout = createStaticLayout(text, targetWidth.toInt())
180-
181-
// Scaling strategy: fill the original area as much as possible while maintaining layout consistency
182-
if (layout.height > targetHeight * 1.1f) {
183-
while (layout.height > targetHeight * 1.1f && textPaint.textSize > 12f) {
184-
textPaint.textSize -= 0.5f
185-
layout = createStaticLayout(text, targetWidth.toInt())
177+
178+
// 1. Set font size based on median box height
179+
val medianBoxHeight = block.lines.map { it.bounds.height().toFloat() }.median()
180+
textPaint.textSize = medianBoxHeight.coerceIn(12f, 120f)
181+
182+
// Get font metrics for precise height calculation
183+
var fm = textPaint.fontMetrics
184+
var fontHeight = fm.descent - fm.ascent
185+
186+
// 2. Calculate line spacing multiplier precisely to match original line distance
187+
// Use fontHeight as denominator to eliminate downward drift
188+
val lineSpacingMulti = if (block.lines.size > 1) {
189+
val dist = block.lines.zipWithNext { a, b -> (b.bounds.top - a.bounds.top).toFloat() }
190+
.filter { it > 0 }
191+
if (dist.isNotEmpty()) (dist.median() / fontHeight).coerceIn(0.5f, 3.0f) else 1.0f
192+
} else 1.0f
193+
194+
fun buildLayout() = StaticLayout.Builder.obtain(text, 0, text.length, textPaint, max(10, targetWidth.toInt()))
195+
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
196+
.setLineSpacing(0f, lineSpacingMulti)
197+
.setIncludePad(false)
198+
.apply {
199+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
200+
setUseLineSpacingFromFallbacks(false)
201+
}
186202
}
187-
} else if (layout.lineCount == 1 && layout.height < targetHeight * 0.8f) {
188-
// If only one line and there's plenty of height, scale up to fill width or height
189-
while (layout.height < targetHeight * 0.9f && getMaxLineWidth(layout) < targetWidth * 0.9f && textPaint.textSize < originalLineHeight * 1.2f) {
190-
textPaint.textSize += 0.5f
191-
layout = createStaticLayout(text, targetWidth.toInt())
203+
.build()
204+
205+
var layout = buildLayout()
206+
207+
// 3. Scaling strategy: shrink text if it wraps more than expected or exceeds target height
208+
val expectedLineCount = max(1, block.lines.size)
209+
val shouldShrink = {
210+
layout.lineCount > expectedLineCount || (expectedLineCount > 1 && layout.height > targetHeight * 1.05f)
211+
}
212+
213+
if (shouldShrink()) {
214+
while (shouldShrink() && textPaint.textSize > 12f) {
215+
textPaint.textSize -= 0.5f
216+
layout = buildLayout()
192217
}
218+
// Update metrics after final size is decided
219+
fm = textPaint.fontMetrics
220+
fontHeight = fm.descent - fm.ascent
193221
}
194222

223+
195224
val actualTextWidth = getMaxLineWidth(layout)
196225
val finalWidth = max(targetWidth, actualTextWidth)
197226
val finalHeight = max(targetHeight, layout.height.toFloat())
198227

228+
// 4. Calculate vertical offset to center the font in the box height
229+
val verticalOffset = (medianBoxHeight - fontHeight) / 2f
230+
199231
val drawRect = RectF(left, top, left + finalWidth, top + finalHeight)
200232

201233
// Check if the drawing area exceeds the current view bounds (screen bounds)
@@ -206,7 +238,8 @@ class OverlayView @JvmOverloads constructor(
206238
// Use a rectangular box for a "textbox" feel and better coverage of original text
207239
canvas.drawRect(drawRect, bgPaint)
208240

209-
canvas.withTranslation(drawRect.left, drawRect.top) {
241+
// Apply translation with vertical centering offset
242+
canvas.withTranslation(drawRect.left, drawRect.top + verticalOffset) {
210243
layout.draw(this)
211244
}
212245

@@ -230,17 +263,25 @@ class OverlayView @JvmOverloads constructor(
230263
bgPaint.color = Color.argb(alpha, Color.red(it), Color.green(it), Color.blue(it))
231264
}
232265
}
233-
234-
val originalColWidth = block.firstLineBounds.width().toFloat()
235-
var bestTextSize = originalColWidth.coerceIn(12f, 120f)
236-
266+
267+
// Prioritize median column width for font size
268+
val medianColWidth = block.lines.map { it.bounds.width().toFloat() }.median()
269+
var bestTextSize = medianColWidth.coerceIn(12f, 120f)
270+
271+
// Calculate column spacing multiplier from original lines
272+
val colSpacingMulti = if (block.lines.size > 1) {
273+
val dist = block.lines.zipWithNext { a, b -> kotlin.math.abs(b.bounds.left - a.bounds.left).toFloat() }
274+
.filter { it > 0 }
275+
if (dist.isNotEmpty()) (dist.median() / medianColWidth).coerceIn(0.8f, 2.5f) else 1.2f
276+
} else 1.2f
277+
237278
fun layoutVertical(size: Float): List<List<String>> {
238279
textPaint.textSize = size
239280
val paragraphs = text.split("\n")
240281
val allCols = mutableListOf<List<String>>()
241282
val charHeight = size * 1.1f
242283
val maxCharsPerCol = (targetHeight / charHeight).toInt().coerceAtLeast(1)
243-
284+
244285
paragraphs.forEach { para ->
245286
if (para.isEmpty()) {
246287
allCols.add(emptyList())
@@ -257,17 +298,17 @@ class OverlayView @JvmOverloads constructor(
257298
}
258299

259300
var columnGroups = layoutVertical(bestTextSize)
260-
val colWidth = bestTextSize * 1.2f
301+
val colWidth = bestTextSize * colSpacingMulti
261302

262303
// Adjust vertical layout to match the original width
263304
if (columnGroups.size * colWidth > targetWidth * 1.1f) {
264-
while (columnGroups.size * (bestTextSize * 1.2f) > targetWidth * 1.1f && bestTextSize > 12f) {
305+
while (columnGroups.size * (bestTextSize * colSpacingMulti) > targetWidth * 1.1f && bestTextSize > 12f) {
265306
bestTextSize -= 0.5f
266307
columnGroups = layoutVertical(bestTextSize)
267308
}
268309
}
269310

270-
val finalColWidth = bestTextSize * 1.2f
311+
val finalColWidth = bestTextSize * colSpacingMulti
271312
val totalColsWidth = columnGroups.size * finalColWidth
272313
val finalWidth = max(targetWidth, totalColsWidth)
273314

@@ -280,14 +321,19 @@ class OverlayView @JvmOverloads constructor(
280321

281322
canvas.drawRect(drawRect, bgPaint)
282323

324+
val vFm = textPaint.fontMetrics
325+
val vFontHeight = vFm.descent - vFm.ascent
326+
val vOffset = (bestTextSize - vFontHeight) / 2f
327+
283328
columnGroups.forEachIndexed { colIdx, chars ->
284329
// Vertical text is usually right-to-left
285330
val colX = drawRect.right - (colIdx + 1) * finalColWidth
286-
var currentY = drawRect.top
331+
var currentY = drawRect.top + vOffset
287332

288333
chars.forEach { charStr ->
289334
val charW = textPaint.measureText(charStr)
290-
canvas.drawText(charStr, colX + (finalColWidth - charW) / 2, currentY + bestTextSize, textPaint)
335+
// Draw text using ascent for precise baseline alignment
336+
canvas.drawText(charStr, colX + (finalColWidth - charW) / 2, currentY - vFm.ascent, textPaint)
291337
currentY += bestTextSize * 1.1f
292338
}
293339
}
@@ -306,12 +352,14 @@ class OverlayView @JvmOverloads constructor(
306352
return maxW
307353
}
308354

309-
private fun createStaticLayout(text: String, width: Int): StaticLayout {
310-
return StaticLayout.Builder.obtain(text, 0, text.length, textPaint, max(10, width))
311-
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
312-
.setLineSpacing(0f, 1.0f)
313-
.setIncludePad(false)
314-
.build()
355+
private fun List<Float>.median(): Float {
356+
if (isEmpty()) throw NoSuchElementException("Cannot calculate median of an empty list")
357+
val sorted = sorted()
358+
return if (size % 2 == 0) {
359+
(sorted[size / 2 - 1] + sorted[size / 2]) / 2f
360+
} else {
361+
sorted[size / 2]
362+
}
315363
}
316364

317365
private fun calculateDelay(): Long {
@@ -320,7 +368,7 @@ class OverlayView @JvmOverloads constructor(
320368
}
321369

322370
val currentTime = System.currentTimeMillis()
323-
371+
324372
// Explicit check: whether in "streaming translation" mode that requires cumulative budget
325373
val isStreamingFlow = settings.enableStreaming && !settings.ocrOnly
326374

0 commit comments

Comments
 (0)