Skip to content
Open
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Comment thread
eszlamczyk marked this conversation as resolved.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 1 addition & 23 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,25 +47,6 @@ android {
}
}

packaging {
jniLibs {
// Android test APK builds merge native libraries from this module and
// react-android. AndroidMath also ships libc++_shared.so, so keep a
// single copy to avoid duplicate native library failures in Detox and
// other instrumentation builds. Remove this workaround after migrating
// away from AndroidMath if the new math backend does not package it.
pickFirsts += ["**/libc++_shared.so"]
}

resources {
// AndroidMath pulls in appcompat/lifecycle dependencies that can bring
// both coroutines-core and coroutines-android into androidTest packaging.
// They ship the same ProGuard metadata file, so keep one copy to avoid
// mergeDebugAndroidTestJavaResource failures. Revisit with AndroidMath.
pickFirsts += ["META-INF/com.android.tools/proguard/coroutines.pro"]
}
}

lintOptions {
disable "GradleCompatible"
}
Expand All @@ -91,7 +72,6 @@ android {
repositories {
mavenCentral()
google()
maven { url 'https://jitpack.io' }
}

def kotlin_version = getExtOrDefault("kotlinVersion")
Expand All @@ -103,9 +83,7 @@ dependencies {
implementation "androidx.profileinstaller:profileinstaller:1.3.1"
// LaTeX math rendering (optional — set enrichedMarkdown.enableMath=false in gradle.properties to exclude)
if (enableMath) {
implementation("com.github.gregcockroft:AndroidMath:v1.1.0") {
exclude group: 'com.google.guava', module: 'guava'
}
implementation("io.github.erweixin:ratex-android:0.1.10")
}
}

Expand Down
5 changes: 2 additions & 3 deletions android/consumer-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
-keep class com.swmansion.enriched.markdown.views.MathContainerView { *; }
-keep class com.swmansion.enriched.markdown.renderer.MathInlineRenderer { *; }

# AndroidMath and its FreeType dependency use JNI to instantiate Java objects from native code.
# RaTeX uses JNI to instantiate Java objects from native code.
# R8 cannot trace these lookups and will strip or rename the referenced classes.
-keep class com.agog.mathdisplay.** { *; }
-keep class com.pvporbit.freetype.** { *; }
-keep class io.ratex.** { *; }
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,8 @@ class EnrichedMarkdown
.newInstance(context, style) as View
resolvedClass.getMethod("applyLatex", String::class.java).invoke(view, segment.latex)
view
} catch (_: Exception) {
} catch (e: Exception) {
Log.e(TAG, "Failed to create math view", e)
View(context)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ object MeasurementStore {
for (i in mathSegmentIndices.indices) {
val metrics = mathResults[i]
mathHeightByIndex[mathSegmentIndices[i]] =
(metrics.ascent + metrics.descent).toInt() + (style.mathStyle.padding * 2)
ceil(metrics.ascent + metrics.descent).toInt() + (style.mathStyle.padding * 2)
}
}

Expand Down Expand Up @@ -607,7 +607,7 @@ object MeasurementStore {
}
return try {
val mathMeasureHelperClass = Class.forName("com.swmansion.enriched.markdown.spans.MathMeasureHelper")
val method = mathMeasureHelperClass.getMethod("measureOnMainThread", Context::class.java, List::class.java)
val method = mathMeasureHelperClass.getMethod("measure", Context::class.java, List::class.java)
@Suppress("UNCHECKED_CAST")
method.invoke(null, context, requests) as List<MathMetrics>
} catch (_: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ fun SpannableString.replaceMathSpansWithPlaceholders(context: Context) {
}

val mathMeasureHelperClass = Class.forName("com.swmansion.enriched.markdown.spans.MathMeasureHelper")
val measureMethod = mathMeasureHelperClass.getMethod("measureOnMainThread", Context::class.java, List::class.java)
val measureMethod = mathMeasureHelperClass.getMethod("measure", Context::class.java, List::class.java)

@Suppress("UNCHECKED_CAST")
val results = measureMethod.invoke(null, context, requests) as? List<MathMetrics> ?: return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.text.SpannableStringBuilder
import com.swmansion.enriched.markdown.parser.MarkdownASTNode
import com.swmansion.enriched.markdown.spans.MathInlineSpan
import com.swmansion.enriched.markdown.utils.text.span.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
import io.ratex.RaTeXFontLoader

class MathInlineRenderer(
private val config: RendererConfig,
Expand All @@ -17,6 +18,8 @@ class MathInlineRenderer(
onLinkLongPress: ((String) -> Unit)?,
factory: RendererFactory,
) {
RaTeXFontLoader.ensureLoaded(context)

val latex = extractLatex(node)
if (latex.isEmpty()) return

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.text.style.ReplacementSpan
import android.view.View.MeasureSpec
import com.agog.mathdisplay.MTMathView
import kotlin.math.roundToInt
import io.ratex.RaTeXEngine
import io.ratex.RaTeXFontLoader
import io.ratex.RaTeXRenderer
import kotlin.math.ceil

class MathInlineSpan(
private val context: Context,
Expand All @@ -26,28 +27,21 @@ class MathInlineSpan(
if (renderFailed) return

try {
val mathView =
MTMathView(context).apply {
labelMode = MTMathView.MTMathViewMode.KMTMathViewModeText
textAlignment = MTMathView.MTTextAlignment.KMTTextAlignmentLeft
this.fontSize = this@MathInlineSpan.fontSize
this.textColor = this@MathInlineSpan.textColor
this.latex = this@MathInlineSpan.latex
}
val displayList = RaTeXEngine.parseBlocking(latex, displayMode = false, color = textColor)
val renderer = RaTeXRenderer(displayList, fontSize) { RaTeXFontLoader.getTypeface(it) }

val spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
mathView.measure(spec, spec)
cachedWidth = renderer.widthPx.toInt().coerceAtLeast(1)
mathAscent = renderer.heightPx
mathDescent = renderer.depthPx

val width = mathView.measuredWidth.coerceAtLeast(1)
val height = mathView.measuredHeight.coerceAtLeast(1)
val bitmap =
Bitmap.createBitmap(
cachedWidth,
ceil(renderer.totalHeightPx).toInt().coerceAtLeast(1),
Bitmap.Config.ARGB_8888,
)

cachedWidth = width
calculateMetrics(mathView, height)

mathView.layout(0, 0, width, height)

val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
mathView.draw(Canvas(bitmap))
renderer.draw(Canvas(bitmap))
cachedBitmap = bitmap
} catch (_: Exception) {
renderFailed = true
Expand All @@ -58,25 +52,6 @@ class MathInlineSpan(
}
}

private fun calculateMetrics(
view: MTMathView,
height: Int,
) {
try {
val dl = DISPLAY_LIST_FIELD?.get(view)
if (dl != null) {
mathAscent = GET_ASCENT_METHOD?.invoke(dl) as? Float ?: (height * 0.7f)
mathDescent = GET_DESCENT_METHOD?.invoke(dl) as? Float ?: (height * 0.3f)
} else {
mathAscent = height * 0.7f
mathDescent = height * 0.3f
}
} catch (e: Exception) {
mathAscent = height * 0.7f
mathDescent = height * 0.3f
}
}

override fun getSize(
paint: Paint,
text: CharSequence?,
Expand All @@ -87,9 +62,10 @@ class MathInlineSpan(
prepareResources()

fm?.apply {
ascent = -mathAscent.roundToInt()
val ascentPx = ceil(mathAscent).toInt()
ascent = -ascentPx
top = ascent
descent = mathDescent.roundToInt()
descent = (cachedBitmap?.height ?: ceil(mathAscent + mathDescent).toInt()) - ascentPx
bottom = descent
}

Expand All @@ -109,25 +85,8 @@ class MathInlineSpan(
) {
prepareResources()
cachedBitmap?.let {
val bitmapY = y - mathAscent
val bitmapY = y - ceil(mathAscent)
canvas.drawBitmap(it, x, bitmapY, paint)
}
}

companion object {
private val DISPLAY_LIST_FIELD =
runCatching {
MTMathView::class.java.getDeclaredField("displayList").apply { isAccessible = true }
}.getOrNull()

private val GET_ASCENT_METHOD =
runCatching {
DISPLAY_LIST_FIELD?.type?.getMethod("getAscent")
}.getOrNull()

private val GET_DESCENT_METHOD =
runCatching {
DISPLAY_LIST_FIELD?.type?.getMethod("getDescent")
}.getOrNull()
}
}
Original file line number Diff line number Diff line change
@@ -1,84 +1,46 @@
package com.swmansion.enriched.markdown.spans

import android.content.Context
import android.os.Handler
import android.os.Looper
import android.view.View.MeasureSpec
import com.agog.mathdisplay.MTMathView
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import android.util.Log
import io.ratex.RaTeXEngine
import io.ratex.RaTeXFontLoader
import io.ratex.RaTeXRenderer

object MathMeasureHelper {
private const val BASE_TIMEOUT_MS = 500L
private const val PER_ITEM_TIMEOUT_MS = 50L
private val mainHandler = Handler(Looper.getMainLooper())
private var sharedMathView: MTMathView? = null

@JvmStatic
fun measureOnMainThread(
fun measure(
context: Context,
requests: List<MathMeasureRequest>,
): List<MathMetrics> {
if (requests.isEmpty()) return emptyList()

if (Looper.myLooper() == Looper.getMainLooper()) {
return requests.map { measureSingle(context, it) }
}

val results = mutableListOf<MathMetrics?>()
val latch = CountDownLatch(1)
val timeout = BASE_TIMEOUT_MS + (PER_ITEM_TIMEOUT_MS * requests.size)

mainHandler.post {
requests.mapTo(results) { request ->
runCatching { measureSingle(context, request) }.getOrNull()
}
latch.countDown()
}

val completed = latch.await(timeout, TimeUnit.MILLISECONDS)
RaTeXFontLoader.ensureLoaded(context)

return requests.mapIndexed { i, req ->
if (completed) {
results.getOrNull(i) ?: estimateFallback(req)
} else {
estimateFallback(req)
}
return requests.map { request ->
runCatching { measureSingle(context, request) }.getOrElse { estimateFallback(request) }
}
}

private fun measureSingle(
context: Context,
request: MathMeasureRequest,
): MathMetrics {
val mathView =
(
sharedMathView ?: MTMathView(context.applicationContext).also {
sharedMathView = it
}
).apply {
labelMode =
when (request.mode) {
MathRenderMode.Display -> MTMathView.MTMathViewMode.KMTMathViewModeDisplay
else -> MTMathView.MTMathViewMode.KMTMathViewModeText
}
textAlignment = MTMathView.MTTextAlignment.KMTTextAlignmentLeft
fontSize = request.fontSize
latex = request.latex
val displayList =
RaTeXEngine.parseBlocking(
request.latex,
displayMode = request.mode == MathRenderMode.Display,
)

val renderer =
RaTeXRenderer(displayList, request.fontSize) {
RaTeXFontLoader.getTypeface(it)
}

val spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
mathView.measure(spec, spec)

val width = mathView.measuredWidth.coerceAtLeast(1)
val height = mathView.measuredHeight.coerceAtLeast(1).toFloat()

return runCatching {
val dl = displayListField?.get(mathView) ?: throw Exception()
val ascent = getAscentMethod?.invoke(dl) as Float
val descent = getDescentMethod?.invoke(dl) as Float
MathMetrics(width, ascent, descent)
}.getOrDefault(MathMetrics(width, height * 0.7f, height * 0.3f))
return MathMetrics(
renderer.widthPx.toInt(),
renderer.heightPx,
renderer.depthPx,
)
}

private fun estimateFallback(request: MathMeasureRequest): MathMetrics {
Expand All @@ -92,12 +54,4 @@ object MathMeasureHelper {
descent = h * 0.3f,
)
}

private val displayListField =
runCatching {
MTMathView::class.java.getDeclaredField("displayList").apply { isAccessible = true }
}.getOrNull()

private val getAscentMethod = runCatching { displayListField?.type?.getMethod("getAscent") }.getOrNull()
private val getDescentMethod = runCatching { displayListField?.type?.getMethod("getDescent") }.getOrNull()
}
Loading
Loading