From 280f36e701f8fc5dac9c1e59aed2219b372ada59 Mon Sep 17 00:00:00 2001 From: manas-yu Date: Sat, 4 Jan 2025 02:31:34 +0530 Subject: [PATCH 01/10] initial --- .../util/parser/html/MathTagHandler.kt | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt index 42ce1a0676e..2ff22bd663a 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt @@ -2,6 +2,9 @@ package org.oppia.android.util.parser.html import android.app.Application import android.content.res.AssetManager +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.drawable.Drawable import android.text.Editable import android.text.Spannable import android.text.style.ImageSpan @@ -60,7 +63,7 @@ class MathTagHandler( } is MathContent.MathAsLatex -> { if (cacheLatexRendering) { - ImageSpan( + CenteredLatexImageSpan( imageRetriever.loadMathDrawable( content.rawLatex, lineHeight, @@ -139,3 +142,71 @@ class MathTagHandler( } } } + +private class CenteredLatexImageSpan(drawable: Drawable?) : ImageSpan(drawable ?: createEmptyDrawable()) { + override fun getSize( + paint: Paint, + text: CharSequence, + start: Int, + end: Int, + fontMetricsInt: Paint.FontMetricsInt? + ): Int { + val d = drawable + val bounds = d.bounds + + fontMetricsInt?.let { + val paintFm = paint.fontMetricsInt + val textHeight = paintFm.descent - paintFm.ascent + val latexHeight = bounds.height() + + val centeringOffset = (textHeight - latexHeight) / 2 + + it.ascent = paintFm.ascent + centeringOffset + it.top = paintFm.top + centeringOffset + it.descent = it.ascent + latexHeight + it.bottom = it.top + latexHeight + } + + return bounds.right + } + + override fun draw( + canvas: Canvas, + text: CharSequence, + start: Int, + end: Int, + x: Float, + top: Int, + y: Int, + bottom: Int, + paint: Paint + ) { + val d = drawable + canvas.save() + + val fontMetrics = paint.fontMetricsInt + val latexHeight = d.bounds.height() + + val centerY = y + (fontMetrics.descent + fontMetrics.ascent) / 2 + val drawableY = centerY - (latexHeight / 2) + + canvas.translate(x, drawableY.toFloat()) + d.draw(canvas) + canvas.restore() + } + + companion object { + private fun createEmptyDrawable(): Drawable { + return object : Drawable() { + override fun draw(canvas: Canvas) {} + override fun setAlpha(alpha: Int) {} + override fun setColorFilter(colorFilter: android.graphics.ColorFilter?) {} + override fun getOpacity(): Int = android.graphics.PixelFormat.TRANSPARENT + + init { + setBounds(0, 0, 1, 1) + } + } + } + } +} From b017712cf724dcfa52d8165a396bae16feb3724b Mon Sep 17 00:00:00 2001 From: manas-yu Date: Sat, 4 Jan 2025 02:33:16 +0530 Subject: [PATCH 02/10] formatting --- .../java/org/oppia/android/util/parser/html/MathTagHandler.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt index 2ff22bd663a..99a38232389 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt @@ -143,7 +143,8 @@ class MathTagHandler( } } -private class CenteredLatexImageSpan(drawable: Drawable?) : ImageSpan(drawable ?: createEmptyDrawable()) { +private class CenteredLatexImageSpan(drawable: Drawable?) : + ImageSpan(drawable ?: createEmptyDrawable()) { override fun getSize( paint: Paint, text: CharSequence, From b691a20841d0640a275165170241c9a22421ac69 Mon Sep 17 00:00:00 2001 From: manas-yu Date: Tue, 7 Jan 2025 02:34:28 +0530 Subject: [PATCH 03/10] revert --- .../util/parser/html/MathTagHandler.kt | 74 +------------------ 1 file changed, 1 insertion(+), 73 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt index 99a38232389..42ce1a0676e 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt @@ -2,9 +2,6 @@ package org.oppia.android.util.parser.html import android.app.Application import android.content.res.AssetManager -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.drawable.Drawable import android.text.Editable import android.text.Spannable import android.text.style.ImageSpan @@ -63,7 +60,7 @@ class MathTagHandler( } is MathContent.MathAsLatex -> { if (cacheLatexRendering) { - CenteredLatexImageSpan( + ImageSpan( imageRetriever.loadMathDrawable( content.rawLatex, lineHeight, @@ -142,72 +139,3 @@ class MathTagHandler( } } } - -private class CenteredLatexImageSpan(drawable: Drawable?) : - ImageSpan(drawable ?: createEmptyDrawable()) { - override fun getSize( - paint: Paint, - text: CharSequence, - start: Int, - end: Int, - fontMetricsInt: Paint.FontMetricsInt? - ): Int { - val d = drawable - val bounds = d.bounds - - fontMetricsInt?.let { - val paintFm = paint.fontMetricsInt - val textHeight = paintFm.descent - paintFm.ascent - val latexHeight = bounds.height() - - val centeringOffset = (textHeight - latexHeight) / 2 - - it.ascent = paintFm.ascent + centeringOffset - it.top = paintFm.top + centeringOffset - it.descent = it.ascent + latexHeight - it.bottom = it.top + latexHeight - } - - return bounds.right - } - - override fun draw( - canvas: Canvas, - text: CharSequence, - start: Int, - end: Int, - x: Float, - top: Int, - y: Int, - bottom: Int, - paint: Paint - ) { - val d = drawable - canvas.save() - - val fontMetrics = paint.fontMetricsInt - val latexHeight = d.bounds.height() - - val centerY = y + (fontMetrics.descent + fontMetrics.ascent) / 2 - val drawableY = centerY - (latexHeight / 2) - - canvas.translate(x, drawableY.toFloat()) - d.draw(canvas) - canvas.restore() - } - - companion object { - private fun createEmptyDrawable(): Drawable { - return object : Drawable() { - override fun draw(canvas: Canvas) {} - override fun setAlpha(alpha: Int) {} - override fun setColorFilter(colorFilter: android.graphics.ColorFilter?) {} - override fun getOpacity(): Int = android.graphics.PixelFormat.TRANSPARENT - - init { - setBounds(0, 0, 1, 1) - } - } - } - } -} From 29f1c03fabe4cbb108ca5e1bc1c82d91e9494cb5 Mon Sep 17 00:00:00 2001 From: manas-yu Date: Tue, 7 Jan 2025 20:03:47 +0530 Subject: [PATCH 04/10] adding tests --- .../util/parser/html/MathTagHandler.kt | 75 ++++++++++- .../util/parser/html/MathTagHandlerTest.kt | 125 ++++++++++++++++++ 2 files changed, 199 insertions(+), 1 deletion(-) diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt index 42ce1a0676e..2698e13566f 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt @@ -2,6 +2,9 @@ package org.oppia.android.util.parser.html import android.app.Application import android.content.res.AssetManager +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.drawable.Drawable import android.text.Editable import android.text.Spannable import android.text.style.ImageSpan @@ -60,7 +63,7 @@ class MathTagHandler( } is MathContent.MathAsLatex -> { if (cacheLatexRendering) { - ImageSpan( + CenteredLatexImageSpan( imageRetriever.loadMathDrawable( content.rawLatex, lineHeight, @@ -139,3 +142,73 @@ class MathTagHandler( } } } + +/** An [ImageSpan] that vertically centers a LaTeX drawable within the surrounding text. */ +private class CenteredLatexImageSpan(drawable: Drawable?) : + ImageSpan(drawable ?: createEmptyDrawable()) { + override fun getSize( + paint: Paint, + text: CharSequence, + start: Int, + end: Int, + fontMetricsInt: Paint.FontMetricsInt? + ): Int { + val d = drawable + val bounds = d.bounds + + fontMetricsInt?.let { + val paintFm = paint.fontMetricsInt + val textHeight = paintFm.descent - paintFm.ascent + val latexHeight = bounds.height() + + val centeringOffset = (textHeight - latexHeight) / 2 + + it.ascent = paintFm.ascent + centeringOffset + it.top = paintFm.top + centeringOffset + it.descent = it.ascent + latexHeight + it.bottom = it.top + latexHeight + } + + return bounds.right + } + + override fun draw( + canvas: Canvas, + text: CharSequence, + start: Int, + end: Int, + x: Float, + top: Int, + y: Int, + bottom: Int, + paint: Paint + ) { + val d = drawable + canvas.save() + + val fontMetrics = paint.fontMetricsInt + val latexHeight = d.bounds.height() + + val centerY = y + (fontMetrics.descent + fontMetrics.ascent) / 2 + val drawableY = centerY - (latexHeight / 2) + + canvas.translate(x, drawableY.toFloat()) + d.draw(canvas) + canvas.restore() + } + + companion object { + private fun createEmptyDrawable(): Drawable { + return object : Drawable() { + override fun draw(canvas: Canvas) {} + override fun setAlpha(alpha: Int) {} + override fun setColorFilter(colorFilter: android.graphics.ColorFilter?) {} + override fun getOpacity(): Int = android.graphics.PixelFormat.TRANSPARENT + + init { + setBounds(0, 0, 1, 1) + } + } + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt index 535d5c90e58..25cea76f5e5 100644 --- a/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt @@ -2,7 +2,9 @@ package org.oppia.android.util.parser.html import android.app.Application import android.content.Context +import android.graphics.Canvas import android.graphics.Color +import android.graphics.Paint import android.text.Html import android.text.Spannable import android.text.style.ImageSpan @@ -39,7 +41,10 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +import kotlin.math.abs import kotlin.reflect.KClass +import org.mockito.Mockito.mock +import org.mockito.Mockito.eq private const val MATH_MARKUP_1 = " Date: Tue, 7 Jan 2025 20:05:08 +0530 Subject: [PATCH 05/10] formatting --- .../org/oppia/android/util/parser/html/MathTagHandlerTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt index 25cea76f5e5..dd9a6620083 100644 --- a/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt @@ -23,6 +23,8 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.eq +import org.mockito.Mockito.mock import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions @@ -43,8 +45,6 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.math.abs import kotlin.reflect.KClass -import org.mockito.Mockito.mock -import org.mockito.Mockito.eq private const val MATH_MARKUP_1 = " Date: Thu, 9 Jan 2025 23:33:16 +0530 Subject: [PATCH 06/10] renaming --- .../java/org/oppia/android/util/parser/html/MathTagHandler.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt index 2698e13566f..1d30e24e2ee 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt @@ -63,7 +63,7 @@ class MathTagHandler( } is MathContent.MathAsLatex -> { if (cacheLatexRendering) { - CenteredLatexImageSpan( + LatexImageSpan( imageRetriever.loadMathDrawable( content.rawLatex, lineHeight, @@ -144,7 +144,7 @@ class MathTagHandler( } /** An [ImageSpan] that vertically centers a LaTeX drawable within the surrounding text. */ -private class CenteredLatexImageSpan(drawable: Drawable?) : +private class LatexImageSpan(drawable: Drawable?) : ImageSpan(drawable ?: createEmptyDrawable()) { override fun getSize( paint: Paint, From dc73c7cf0a935e08f01df5ed60640a122929d2a6 Mon Sep 17 00:00:00 2001 From: manas-yu Date: Thu, 23 Jan 2025 01:34:45 +0530 Subject: [PATCH 07/10] renaming --- .../util/parser/html/MathTagHandler.kt | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt index 1591ca60548..b121e1ad731 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt @@ -151,6 +151,7 @@ class MathTagHandler( /** An [ImageSpan] that vertically centers a LaTeX drawable within the surrounding text. */ private class LatexImageSpan(drawable: Drawable?) : ImageSpan(drawable ?: createEmptyDrawable()) { + override fun getSize( paint: Paint, text: CharSequence, @@ -158,23 +159,23 @@ private class LatexImageSpan(drawable: Drawable?) : end: Int, fontMetricsInt: Paint.FontMetricsInt? ): Int { - val d = drawable - val bounds = d.bounds + val latexDrawable = drawable + val drawableBounds = latexDrawable.bounds - fontMetricsInt?.let { - val paintFm = paint.fontMetricsInt - val textHeight = paintFm.descent - paintFm.ascent - val latexHeight = bounds.height() + fontMetricsInt?.let { metrics -> + val paintFontMetrics = paint.fontMetricsInt + val textHeight = paintFontMetrics.descent - paintFontMetrics.ascent + val latexHeight = drawableBounds.height() val centeringOffset = (textHeight - latexHeight) / 2 - it.ascent = paintFm.ascent + centeringOffset - it.top = paintFm.top + centeringOffset - it.descent = it.ascent + latexHeight - it.bottom = it.top + latexHeight + metrics.ascent = paintFontMetrics.ascent + centeringOffset + metrics.top = paintFontMetrics.top + centeringOffset + metrics.descent = metrics.ascent + latexHeight + metrics.bottom = metrics.top + latexHeight } - return bounds.right + return drawableBounds.right } override fun draw( @@ -188,17 +189,17 @@ private class LatexImageSpan(drawable: Drawable?) : bottom: Int, paint: Paint ) { - val d = drawable + val latexDrawable = drawable canvas.save() val fontMetrics = paint.fontMetricsInt - val latexHeight = d.bounds.height() + val latexHeight = latexDrawable.bounds.height() val centerY = y + (fontMetrics.descent + fontMetrics.ascent) / 2 val drawableY = centerY - (latexHeight / 2) canvas.translate(x, drawableY.toFloat()) - d.draw(canvas) + latexDrawable.draw(canvas) canvas.restore() } From cb120e1a18e19e794cf308867e1e4149bacb5b9a Mon Sep 17 00:00:00 2001 From: manas-yu Date: Fri, 24 Jan 2025 01:40:27 +0530 Subject: [PATCH 08/10] clipping issue --- .../util/parser/html/MathTagHandler.kt | 64 +++++++------------ 1 file changed, 24 insertions(+), 40 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt index b121e1ad731..7b71ac6d699 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt @@ -149,8 +149,7 @@ class MathTagHandler( } /** An [ImageSpan] that vertically centers a LaTeX drawable within the surrounding text. */ -private class LatexImageSpan(drawable: Drawable?) : - ImageSpan(drawable ?: createEmptyDrawable()) { +private class LatexImageSpan(drawable: Drawable) : ImageSpan(drawable) { override fun getSize( paint: Paint, @@ -159,23 +158,23 @@ private class LatexImageSpan(drawable: Drawable?) : end: Int, fontMetricsInt: Paint.FontMetricsInt? ): Int { - val latexDrawable = drawable - val drawableBounds = latexDrawable.bounds - - fontMetricsInt?.let { metrics -> - val paintFontMetrics = paint.fontMetricsInt - val textHeight = paintFontMetrics.descent - paintFontMetrics.ascent - val latexHeight = drawableBounds.height() - - val centeringOffset = (textHeight - latexHeight) / 2 - - metrics.ascent = paintFontMetrics.ascent + centeringOffset - metrics.top = paintFontMetrics.top + centeringOffset - metrics.descent = metrics.ascent + latexHeight - metrics.bottom = metrics.top + latexHeight + val drawable = drawable + val rect = drawable.bounds + + fontMetricsInt?.let { fm -> + val paintMetrics = paint.fontMetricsInt + val fontHeight = paintMetrics.descent - paintMetrics.ascent + val drawableHeight = rect.bottom - rect.top + val centerY = paintMetrics.ascent + fontHeight / 2 + + // Adjust font metrics to center the drawable vertically + fm.ascent = centerY - drawableHeight / 2 + fm.top = fm.ascent + fm.bottom = centerY + drawableHeight / 2 + fm.descent = fm.bottom } - return drawableBounds.right + return rect.right } override fun draw( @@ -189,32 +188,17 @@ private class LatexImageSpan(drawable: Drawable?) : bottom: Int, paint: Paint ) { - val latexDrawable = drawable + val drawable = drawable canvas.save() - val fontMetrics = paint.fontMetricsInt - val latexHeight = latexDrawable.bounds.height() + // Calculate vertical centering + val paintMetrics = paint.fontMetricsInt + val fontHeight = paintMetrics.descent - paintMetrics.ascent + val centerY = y + paintMetrics.descent - fontHeight / 2 + val transY = centerY - (drawable.bounds.bottom - drawable.bounds.top) / 2 - val centerY = y + (fontMetrics.descent + fontMetrics.ascent) / 2 - val drawableY = centerY - (latexHeight / 2) - - canvas.translate(x, drawableY.toFloat()) - latexDrawable.draw(canvas) + canvas.translate(x, transY.toFloat()) + drawable.draw(canvas) canvas.restore() } - - companion object { - private fun createEmptyDrawable(): Drawable { - return object : Drawable() { - override fun draw(canvas: Canvas) {} - override fun setAlpha(alpha: Int) {} - override fun setColorFilter(colorFilter: android.graphics.ColorFilter?) {} - override fun getOpacity(): Int = android.graphics.PixelFormat.TRANSPARENT - - init { - setBounds(0, 0, 1, 1) - } - } - } - } } From b9b9feaa139841287b6253dd023a20efea1e15ba Mon Sep 17 00:00:00 2001 From: manas-yu Date: Fri, 24 Jan 2025 02:23:06 +0530 Subject: [PATCH 09/10] inline shift approach --- .../util/parser/html/MathTagHandler.kt | 82 ++++++++++++------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt index 7b71ac6d699..7f17e61c27c 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt @@ -7,9 +7,12 @@ import android.graphics.Paint import android.graphics.drawable.Drawable import android.text.Editable import android.text.Spannable +import android.text.style.DynamicDrawableSpan import android.text.style.ImageSpan +import android.text.style.ReplacementSpan import androidx.core.content.res.ResourcesCompat import io.github.karino2.kotlitex.view.MathExpressionSpan +import java.lang.ref.WeakReference import org.json.JSONObject import org.oppia.android.util.R import org.oppia.android.util.logging.ConsoleLogger @@ -68,7 +71,8 @@ class MathTagHandler( content.rawLatex, lineHeight, type = if (useInlineRendering) INLINE_TEXT_IMAGE else BLOCK_IMAGE - ) + ), + useInlineRendering ) } else { MathExpressionSpan( @@ -149,32 +153,45 @@ class MathTagHandler( } /** An [ImageSpan] that vertically centers a LaTeX drawable within the surrounding text. */ -private class LatexImageSpan(drawable: Drawable) : ImageSpan(drawable) { +private class LatexImageSpan( + private val drawable: Drawable, + private val isInline: Boolean +) : ReplacementSpan() { + companion object { + private const val INLINE_SHIFT_FACTOR = 0.9f // Adjust this value (0.2-0.4) as needed + } override fun getSize( paint: Paint, text: CharSequence, start: Int, end: Int, - fontMetricsInt: Paint.FontMetricsInt? + fm: Paint.FontMetricsInt? ): Int { - val drawable = drawable - val rect = drawable.bounds - - fontMetricsInt?.let { fm -> - val paintMetrics = paint.fontMetricsInt - val fontHeight = paintMetrics.descent - paintMetrics.ascent - val drawableHeight = rect.bottom - rect.top - val centerY = paintMetrics.ascent + fontHeight / 2 - - // Adjust font metrics to center the drawable vertically - fm.ascent = centerY - drawableHeight / 2 - fm.top = fm.ascent - fm.bottom = centerY + drawableHeight / 2 - fm.descent = fm.bottom + val bounds = drawable.bounds + val imageHeight = bounds.height() + val paintMetrics = paint.fontMetricsInt + val textHeight = paintMetrics.descent - paintMetrics.ascent + + fm?.let { metrics -> + if (isInline) { + // Reserve space for inline shift + val verticalShift = (imageHeight - textHeight) / 2 + + (paintMetrics.descent * INLINE_SHIFT_FACTOR).toInt() + metrics.ascent = paintMetrics.ascent - verticalShift + metrics.top = metrics.ascent + metrics.descent = paintMetrics.descent + verticalShift + metrics.bottom = metrics.descent + } else { + // Block mode calculations remain unchanged + val totalHeight = (imageHeight * 1.2).toInt() + metrics.ascent = -totalHeight / 2 + metrics.top = metrics.ascent + metrics.descent = totalHeight / 2 + metrics.bottom = metrics.descent + } } - - return rect.right + return bounds.right } override fun draw( @@ -183,21 +200,28 @@ private class LatexImageSpan(drawable: Drawable) : ImageSpan(drawable) { start: Int, end: Int, x: Float, - top: Int, - y: Int, - bottom: Int, + lineTop: Int, + baseline: Int, + lineBottom: Int, paint: Paint ) { - val drawable = drawable canvas.save() - // Calculate vertical centering - val paintMetrics = paint.fontMetricsInt - val fontHeight = paintMetrics.descent - paintMetrics.ascent - val centerY = y + paintMetrics.descent - fontHeight / 2 - val transY = centerY - (drawable.bounds.bottom - drawable.bounds.top) / 2 + val imageHeight = drawable.bounds.height() + val yPosition = when { + isInline -> { + // Apply downward shift for inline equations + val textMidline = baseline - (paint.fontMetrics.descent - paint.fontMetrics.ascent) / 2 + val shiftOffset = (paint.fontMetricsInt.descent * INLINE_SHIFT_FACTOR).toInt() + textMidline - (imageHeight / 2) + shiftOffset + } + else -> { + // Block mode remains centered + lineTop + (lineBottom - lineTop - imageHeight) / 2 + } + } - canvas.translate(x, transY.toFloat()) + canvas.translate(x, yPosition.toFloat()) drawable.draw(canvas) canvas.restore() } From 5e99381a8c9a91126cc7bf0f9fc517b0d1b71cae Mon Sep 17 00:00:00 2001 From: manas-yu Date: Sat, 25 Jan 2025 17:50:35 +0530 Subject: [PATCH 10/10] updating tests --- .../util/parser/html/MathTagHandler.kt | 72 ++++++++++--------- .../util/parser/html/MathTagHandlerTest.kt | 15 ++-- 2 files changed, 48 insertions(+), 39 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt index 7f17e61c27c..8a4de9460c1 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt @@ -7,12 +7,9 @@ import android.graphics.Paint import android.graphics.drawable.Drawable import android.text.Editable import android.text.Spannable -import android.text.style.DynamicDrawableSpan import android.text.style.ImageSpan -import android.text.style.ReplacementSpan import androidx.core.content.res.ResourcesCompat import io.github.karino2.kotlitex.view.MathExpressionSpan -import java.lang.ref.WeakReference import org.json.JSONObject import org.oppia.android.util.R import org.oppia.android.util.logging.ConsoleLogger @@ -154,11 +151,25 @@ class MathTagHandler( /** An [ImageSpan] that vertically centers a LaTeX drawable within the surrounding text. */ private class LatexImageSpan( - private val drawable: Drawable, - private val isInline: Boolean -) : ReplacementSpan() { + imageDrawable: Drawable?, + private val isInlineMode: Boolean +) : ImageSpan(imageDrawable ?: createEmptyDrawable()) { + companion object { - private const val INLINE_SHIFT_FACTOR = 0.9f // Adjust this value (0.2-0.4) as needed + private const val INLINE_VERTICAL_SHIFT_RATIO = 0.9f + + private fun createEmptyDrawable(): Drawable { + return object : Drawable() { + override fun draw(canvas: Canvas) {} + override fun setAlpha(alpha: Int) {} + override fun setColorFilter(colorFilter: android.graphics.ColorFilter?) {} + override fun getOpacity(): Int = android.graphics.PixelFormat.TRANSPARENT + + init { + setBounds(0, 0, 1, 1) + } + } + } } override fun getSize( @@ -166,24 +177,22 @@ private class LatexImageSpan( text: CharSequence, start: Int, end: Int, - fm: Paint.FontMetricsInt? + fontMetrics: Paint.FontMetricsInt? ): Int { - val bounds = drawable.bounds - val imageHeight = bounds.height() - val paintMetrics = paint.fontMetricsInt - val textHeight = paintMetrics.descent - paintMetrics.ascent - - fm?.let { metrics -> - if (isInline) { - // Reserve space for inline shift + val drawableBounds = drawable.bounds + val imageHeight = drawableBounds.height() + val textMetrics = paint.fontMetricsInt + val textHeight = textMetrics.descent - textMetrics.ascent + + fontMetrics?.let { metrics -> + if (isInlineMode) { val verticalShift = (imageHeight - textHeight) / 2 + - (paintMetrics.descent * INLINE_SHIFT_FACTOR).toInt() - metrics.ascent = paintMetrics.ascent - verticalShift + (textMetrics.descent * INLINE_VERTICAL_SHIFT_RATIO).toInt() + metrics.ascent = textMetrics.ascent - verticalShift metrics.top = metrics.ascent - metrics.descent = paintMetrics.descent + verticalShift + metrics.descent = textMetrics.descent + verticalShift metrics.bottom = metrics.descent } else { - // Block mode calculations remain unchanged val totalHeight = (imageHeight * 1.2).toInt() metrics.ascent = -totalHeight / 2 metrics.top = metrics.ascent @@ -191,7 +200,7 @@ private class LatexImageSpan( metrics.bottom = metrics.descent } } - return bounds.right + return drawableBounds.right } override fun draw( @@ -208,20 +217,17 @@ private class LatexImageSpan( canvas.save() val imageHeight = drawable.bounds.height() - val yPosition = when { - isInline -> { - // Apply downward shift for inline equations - val textMidline = baseline - (paint.fontMetrics.descent - paint.fontMetrics.ascent) / 2 - val shiftOffset = (paint.fontMetricsInt.descent * INLINE_SHIFT_FACTOR).toInt() - textMidline - (imageHeight / 2) + shiftOffset - } - else -> { - // Block mode remains centered - lineTop + (lineBottom - lineTop - imageHeight) / 2 - } + val yOffset = if (isInlineMode) { + val metrics = paint.fontMetricsInt + val ascent = metrics.ascent.toFloat() + val descent = metrics.descent.toFloat() + val expectedCenterY = baseline.toFloat() + (ascent + descent) / 2f + expectedCenterY - (imageHeight / 2f) + } else { + lineTop.toFloat() + (lineBottom - lineTop - imageHeight) / 2f } - canvas.translate(x, yPosition.toFloat()) + canvas.translate(x, yOffset) drawable.draw(canvas) canvas.restore() } diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt index e91f54d6125..e2a2365b61d 100644 --- a/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt @@ -155,7 +155,6 @@ class MathTagHandlerTest { val metrics = paint.fontMetricsInt val y = 100 - val expectedCenterY = y + (metrics.descent + metrics.ascent) / 2f imageSpans[0].draw( mockCanvas, @@ -168,17 +167,20 @@ class MathTagHandlerTest { 200, paint ) - // The canvas should be translated to center the drawable vertically + + val textHeight = (metrics.descent - metrics.ascent).toFloat() + val textMidline = y.toFloat() - (textHeight / 2f) + val verticalShift = metrics.descent * 0.9f + val drawable = imageSpans[0].drawable + val expectedTranslation = textMidline + verticalShift - (drawable.bounds.height() / 2f) + + // The translation should position the drawable centered around the text baseline verify(mockCanvas).save() verify(mockCanvas).translate( eq(0f), capture(floatCaptor) ) - // The translation should position the drawable centered around the text baseline - val drawable = imageSpans[0].drawable - val expectedTranslation = expectedCenterY - (drawable.bounds.height() / 2) assertThat(floatCaptor.value).isWithin(1f).of(expectedTranslation) - verify(mockCanvas).restore() } @@ -233,6 +235,7 @@ class MathTagHandlerTest { val lineHeight = paint.textSize * 1.2f assertThat(totalHeight.toFloat()).isLessThan(lineHeight) } + @Test fun testParseHtml_emptyString_doesNotIncludeImageSpan() { val parsedHtml =