From 22c0b9e4f71c0b3dfb090f2e2e9d74c740554a60 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 24 Oct 2026 13:27:21 -0700 Subject: [PATCH 1/2] fix(android): center inline images on the cap height like iOS An inline image's bottom sat on the baseline, so an icon taller than the capitals stood above the line of text. iOS centers inline attachments on the font's cap height; Android now draws the image centered on the cap height of the paint it is drawn with. Both ImageSpan copies change the same way. --- .../enriched/markdown/spans/ImageSpan.kt | 6 +- .../markdown/InlineImagePlacementTest.kt | 69 +++++++++++++++++++ .../enriched/markdown/spans/ImageSpan.kt | 6 +- 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/InlineImagePlacementTest.kt diff --git a/packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt b/packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt index 8d3248c49..ed13aba26 100644 --- a/packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +++ b/packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt @@ -8,6 +8,7 @@ import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Path +import android.graphics.Rect import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build @@ -52,6 +53,7 @@ class ImageSpan( private val requestKey: String = ImageCache.requestKey(imageUrl, requestHeaders) private var cachedWidth: Int = 0 + private val capBounds = Rect() private var viewRef: WeakReference? = null private var sourceDrawable: Drawable? = null @@ -221,8 +223,10 @@ class ImageSpan( val drawable = getDrawable() canvas.withSave { if (isInline) { + // Centered on the cap height of the surrounding text, as iOS places inline attachments. val imageHeight = drawable.bounds.height() - translate(x, (y - imageHeight + (imageHeight * 0.1f))) + paint.getTextBounds("H", 0, 1, capBounds) + translate(x, y - (capBounds.height() + imageHeight) / 2f) } else { translate(x, top.toFloat()) } diff --git a/packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/InlineImagePlacementTest.kt b/packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/InlineImagePlacementTest.kt new file mode 100644 index 000000000..4bc29d3d9 --- /dev/null +++ b/packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/InlineImagePlacementTest.kt @@ -0,0 +1,69 @@ +package com.swmansion.enriched.markdown + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Rect +import android.util.TypedValue +import android.view.View +import android.widget.TextView +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.swmansion.enriched.markdown.spans.registerWithSpans +import com.swmansion.enriched.markdown.test.MarkdownRenderTestSupport +import com.swmansion.enriched.markdown.test.MarkdownRenderTestSupport.render +import com.swmansion.enriched.markdown.test.TestAstFactory.document +import com.swmansion.enriched.markdown.test.TestAstFactory.image +import com.swmansion.enriched.markdown.test.TestAstFactory.paragraph +import com.swmansion.enriched.markdown.test.TestAstFactory.text +import com.swmansion.enriched.markdown.utils.text.ImageCache +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [28]) +// Legacy graphics report zero font metrics, so the cap height this checks against would be zero. +@GraphicsMode(GraphicsMode.Mode.NATIVE) +class InlineImagePlacementTest { + private val context: Context = ApplicationProvider.getApplicationContext() + + // iOS centers an inline attachment on the font's cap height; Android used to sit the image on + // the baseline, so an icon taller than the capitals stood above the text. + @Test + fun anInlineImageIsCenteredOnTheCapHeight() { + val url = "test://inline-image-placement" + ImageCache.putOriginal(url, Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888).apply { eraseColor(Color.RED) }) + val style = MarkdownRenderTestSupport.defaultStyle + val rendered = render(document(paragraph(text("Inline "), image(url), text(" image"))), style) + val textView = TextView(context) + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, style.paragraphStyle.fontSize) + textView.text = rendered + textView.registerWithSpans(rendered) + textView.measure( + View.MeasureSpec.makeMeasureSpec(400, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), + ) + textView.layout(0, 0, 400, textView.measuredHeight) + val layout = requireNotNull(textView.layout) + + val bitmap = Bitmap.createBitmap(400, textView.height, Bitmap.Config.ARGB_8888) + layout.draw(Canvas(bitmap)) + val redRows = + (0 until bitmap.height).filter { y -> + (0 until bitmap.width).any { x -> + val pixel = bitmap.getPixel(x, y) + Color.alpha(pixel) > 0 && Color.red(pixel) > 128 && Color.green(pixel) < 64 + } + } + val imageCenter = (redRows.first() + redRows.last() + 1) / 2f + + val capBounds = Rect() + textView.paint.getTextBounds("H", 0, 1, capBounds) + val capCenter = layout.getLineBaseline(0) - capBounds.height() / 2f + assertEquals(capCenter, imageCenter, 1f) + } +} diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt index 2d0109e32..04bc9bf07 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt @@ -6,6 +6,7 @@ import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Path +import android.graphics.Rect import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build @@ -75,6 +76,7 @@ class ImageSpan( private val requestKey: String = ImageCache.requestKey(requestUrl, requestHeaders) private var cachedWidth: Int = 0 + private val capBounds = Rect() private var viewRef: WeakReference? = null private var sourceDrawable: Drawable? = null private var onBoxHeightChanged: (() -> Unit)? = null @@ -313,8 +315,10 @@ class ImageSpan( val drawable = getDrawable() canvas.withSave { if (isInline) { + // Centered on the cap height of the surrounding text, as iOS places inline attachments. val imageHeight = drawable.bounds.height() - translate(x, (y - imageHeight + (imageHeight * 0.1f))) + paint.getTextBounds("H", 0, 1, capBounds) + translate(x, y - (capBounds.height() + imageHeight) / 2f) } else { translate(x, top.toFloat()) } From 7bf92da6f6862419999c9e1259e4f3341edf67b3 Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Mon, 26 Oct 2026 15:51:55 +0100 Subject: [PATCH 2/2] fix(android): grow the line to fit a tall inline image Centered on the cap height, an inline image taller than the text ran past both edges of its line and the view clipped it. iOS grows the line for such an attachment; getSize now widens the line metrics the same way. Re-records the Android inline image screenshots for the new placement. Co-Authored-By: Claude Opus 5.5 --- .../android/inline_image_display.png | Bin 15615 -> 14725 bytes .../android/inline_image_display.png | Bin 17261 -> 14625 bytes .../enriched/markdown/spans/ImageSpan.kt | 26 +++++++++--- .../markdown/InlineImagePlacementTest.kt | 37 ++++++++++++++++-- .../test/MarkdownRenderTestSupport.kt | 7 +++- .../enriched/markdown/spans/ImageSpan.kt | 26 +++++++++--- 6 files changed, 82 insertions(+), 14 deletions(-) diff --git a/.maestro/androidExample/enrichedMarkdownText/screenshots/android/inline_image_display.png b/.maestro/androidExample/enrichedMarkdownText/screenshots/android/inline_image_display.png index 6c1b92b352860677548488c297dab566de1f8ee0..87cc3edfec7eac65dcb5e6fcf05009e5ca43a0cc 100644 GIT binary patch literal 14725 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 KwD8HRcmD_UqMIH7 literal 15615 zcmcJ$XH-*N*EVWLML|KOgHl9_AR=8rMYw^Q$Pd+1f)agEd&U?hNy^0 zmlk@0bP_rQBoH_|-p~Df?>N7{amIW85XRbj&%O3qbIp0pIj77W;QyiXr+~kyr2!r%PCWdf{`9edm&NK7jo0VARv#(^qA%C~@^vm44O&KDxFp#Vq*d|toW9m|Nit)hhpEXL0G*=aL?Wpan(h&kKgyUo9Sbuw|mnC*3ZW3A=wuN9g+6z!pa<5dK0}>1(NcI5{G{@! z&4q4;G{;zZ(5YH^*o5X1N@N|H=F>U+(V@-S9k}=AweTJW_-ay-T6e9TbFOqKDSfgz zZ!mySKF#lO3t#d&Rp8BQ7X}y#NpJ2#O}Xpj!%7$)Ank1D+2vg|wC?<#Ye24;ZC^eK z+!vzsq~@u^^(sC$&5hbZ*FqE=o&sBXnUnf`zq)H74Y=dP9Z`y&`vn{WW;NxZ3pnXz zd7RH=Y{hlOgYgr_PmY8&?qW<0-a6K%nL9X}&_*gTcM2g5(Iy#$NVNlFCv=U6 z&WbYhJBN_0!V622$_C7NfKeBs8G0-#_vua?3Z4;fM^Ri;d*bR^VN8AHBb(SqW$;+u z$LJsVLd)2|IBotk7*)Q3Ncg;=LQ$8?48eLqNsrFU0~!y-MfgT5E$^!7t`}Z;3r7+DwaB_7;YqW zK{%DcG{Dcp2oxRg<8=*6{gRD4-(-QG>F57EnEQYG;8V!+=bH&7GNHF4Y?uIO)A1vN!)opAf}! ztz+W|9$#HXwA&u{e^5GK|B4N^hG2-?O!*$Kf=$Au8@8cOBU7>!L?!s%2@o$4{ z?VJ$VhXMU49$Fp&CAu~`g=))RxKU2FMTuj}&Oil;Kaiu6K4;=bi-t8_Cmd`$>g1Ux z)HRc0$9Omy4vb&jjC{`-9_7S>viq9G)kE&!Iyk2EH63onW;5FQ$EPINwaA}fqb}&3 zievkk>hBV*vrOZ;9tNG$^`n?e8Cx-!wWEy8H{`tonjMb){ox&U_uD7_@KCo0V&*gO zIRw-iZD62Rr-9+v4t(w~ji*$*M{#+Q12L{${a(_ssJ;?6sZBU|5 zyJf0zEWZs&y`TNNs6wxF@O?@!y~sVm=9V|960svM3NMW+;uX#cMEU3(g~>H(f&;UA z(rKB>Q`}E~;R19gXu?PvifgyS1c)BxHd1~ZZe&{=Z{yaE7cst)%rb^<4Oc5^R!leA zqt_`Ldwj6G2bq%4th>=8YZ7EgAYT8y!VY-@C$}&j-}sq{=GjsE01B5g`>t9u1a|Wb zbq(vv?=sQ11g({)sdxit^2e*^v{9ZP-LI*>aGuL!L)k@=(#mC ze8%`m2YgWwM=FwceeuVAap99Aern~8Z(GT=^&;z5 z>OZ0ALY{*6a?&R!Lp2S-^J%M1-CuF>2^sn|w)F!`X>QxtjD_hXDB~kzjBAzsB`B%t zu~0;_#=33 zoi*QX^7x3!d=8%ll}Cybo!i`cWUmaA(i4RYOC1MhU;;i&Vz1W{1&jIf*ZON#9xC*{2sT+F$v)_hFnq5lQYFfeWR z8^RTVWeozG?F`+oKsRK*O_9DZ+H2RYPg-F=bg$mPMoHcz8e)TtJI?M;V+(oa=a#E% z^E;Knd#MpPZ}gX2NI6l3(mPKej$>tpS)VGtlYuu40=`08TWR2Yo%3qYkFdaWP z24lQ!DRA3hJXm&BF_Ud$D!EqzP+|Fx&36JHZgl?Yh4eO-Y<|JS2zV=J7<0Kvdu(K% z-CF;oL~8)X)a_5hO&1X(Bot?<1Gcr3o_+`^;VTj0pY1VD8U5(a6w%v?rjrM@-FRvnGM6VQ3Xx!+F5 zlXv(hTmkF7$E7ey+;a>w7Ny#nV_c>CMt`I2RKUu4V}iuX0Pf8^nQVKz;YG+ZxLBXy zE-g-X-b)DCC1yLg;3(2nN(nfeH^M*MG6-(s@^kC{Qw*K@Hh&IE*}q->zR8fDL&o+Q z?|W4ABf&TSe1W?epuoIa+q7%-bp5ic1Gk3# ztopNo8S=}3^&!eBuJT@OUX3e0P5(Ll=5Y^g@JDf?-hRuC;NxXXtfm>#thjN?*P4*Jc z_Clwa{5D@H$9_62#KO<0x})~|;EkH3)9yS-2f!wD_3%Yvq@50R>`C6<<4TdSHx58E!Zy{$TuiZz|C<#S;-wffc*|qI9ehFBRHoF#9?>7|? z@Sb9o6MA%S!h2OQ0~e@cJYJh=oOJ(=)8i8F_~X?W^wCHK_eVWr5J<> z=bTh>;$Yd^NpY_fv@X?7k%8b`r@Dr2iV-P4+GNfZtgu$DRq4V{U$dFb2MiH zhYatYoU@@vZ=ElluyL)1VSP>F9OnMEEqY53vQ66|gOn2v_+rQ_E|6&iSoaKxzzSpt zDAQ5xne4nGyn{lQBZMD!*|r!+G~jrpdBk|HLm^+V>6I;_r8(Ec*W8!>Ib-Zhdg8Y! zmb*?jCIc@G{+aCxYsW7^rvy*-AtyAyh!YK$>wGQHN9)IfQ&aYbd4rU@VQ==`5;X|H zbizd=&fVx98vK;?ubAUesxMq-+FGQi^C_7Bs8VXT$5pkf;j>iZcwS2JHNHW0c%6&T zK5;%KdETMVV{KbWgWx^P%SqAasmarWpgG%OL=6h;8L%X^YfWA$&D?UJIt8WSRRsiK zbxx1%ox*_%bmm$Gs^)WbX+$9@SF;MbT1MilO5=a0kO6G}xFIcpF zW{KDRr}mLcsoiST=18scx}w)8`&X42BZ|u1)m+SQ4Zc`>@f7Fm)ZoOI`7R@y9&n)I zNFL}*3~%e#R@LF9%4Ct&M&yHiX2*doXGEn1;06b?^%eKD+O)t)D+eo)8VEb?b{;_Z@^0L%=jjwY$d0l7~C&r&0D^om@)d12r$v|GIdLI>VQ5+n*KDzKS zzq3xK=l8Ay_VkSd38K$3+G;J{TxDv^y0{0A*at~H3bKyArBd3(j&!4V8z-}Ri2^@4@5q_sz z4!-h7g)`esqxM$0+Eu`!_?bKEg}uys-zK_t#vYfWKKEtpDI{BSDPK>KGS(wO7W0@t z;m+|k1nhKZknqlX?DrfdF0qlaLB@1tfDxb`kSNZTU;%laD*r+p!UV(JsUTI!lz?;cf^u$Zw>)u5s?qWmbrqD=9lgE%g?4S;RI5ft=zn&L07V0E zh_SG@qA$Th!MH+&%iI+c{D<3u%?YRZ4vAaQ*Xv8uZ&0r-Mqsnux!hfD&eIWNS3W9u z(mF-Yu2ZZsi|mq2=6kLWTChvC^e4NPS`}WJbS`iKS!5{YZ_>=N#juR*N2d6&8|6K@rjrJ)@*4RIsJkg*+d6zdq!uD37@Ql*xF5 zQOD&Yz}_PYZfpb#@JrPx6QdE)%|1oT*uqb$Bb561g} zm{h;S`oO;j!E+FM}KF#pHC%ftWTjXgQFIfvGH-;SXnA-WjXWR9avHI?PD!1`;m zPm!b?&MrhY8O=bu{#JclPe(B4*Bj3e6yf`F|eC62m35i=NKl*c!y8p{% ziM8GSe7B}HeMsuV?B(wl=xf+X6i8zFw?O3YkXN*|q9zs2vsgx%1}G%^s#Cz|{;CBC zqM+CP^(vPbk8@w*)_j=y;9~LZ!jaf=J_3pM6*SQLD=h*Fu8UZ?x2Z}=&ZSNvT0gG+E&FuM}&mR`H8>{6uANv z(HyBTq`*H9X1`Tc8rHz#euFSl(!tr{P1GhSVY)X`syfC;`9Ah`0iny(A5vgni!_BH zI(b^X|1ItuGI#t7blvngp?<+Hsu(FNQAV1`@k6odi!UwGPQX^+J_riAW3n`NOl6@6b}R){4=Yu{tXh2k%zR?=AFB znaR=kuA>!qgbyn90nCc#LI>q?LG1e9O}@E1#Ps8G!FBVwt}j9U+xZU&;>%nb&iDM6 zd-m<($~B(#bz#>03S_cewp+8VT7f7DB_k|KJc=(=aB%;ti!`)S$%f*%i^MHQJvz=? zk^V`N3bk`DuWkIeIVLs)_vllqiJJJc!Wi2phkn=lJBWWS63N=Q7Fqr=`-TjmL|kl( zWS+@#kp9`q)$fQu-ddhb>F(BYF5wc3OfRz_my~_cvmR!9F}jVD@j2q?%V7HXM0y@l zM63M40K*XM?bdG_|D0t_M|xuAZLT8YK32hY%?nv7XS+g*tA?K@`jZ>PhFadflSr_H znn$1XDVHd<2$UL4f9k;a(JL?qaqmUX&%%$9Ssf&tIuc4P1!e)CG z^*V;SH5BknX|+Wcf8?H7{byTRdxiE@I5hY^Pl&2l5*|KZKU<<*A1fl*Bgz|g_6?`~ zgFN6|)Be$qLAp>5;wWdb{pHS0KF+C>G;2xvRenpdyk@6heA zwv+UYH?Gt0omkEkCBJ*{8aocXSfX(JWcX6@wTxxQ>0hyNoR{X7^2_D|8eKJgr%~TI zB8+$tVZJeG5A@M5>4|_B>3m^gGmZpX6>$scYkfa*C_X4)xQFaY9infoguOf4^3w5C ziV_S_5RjWlmpSFuY*g1YwdcZN^@HkV(2;1<)5e+DEUYu`Pqtx!_i<3%e8Y;tN4FDxx*6xahAQ zpNf!|lh*le_?vl%e>`-g+jYhP(8ZpPuFVDOsy8QE?U7>=1S>sN^WM5-- z9|;mr3c=apo2Y7>FRu#c5)L8R7x=t~Srs_}Sq8-Tku=7)aZ2a=qSu$fa-TOX%chty zWen#RO1iXgFQLX}*|Dcll=S!<1^3iHGse8x3@rky7JEh(!@;j94NQD#IUR;%A`wkJ zR>#&Ae_STO5!48V0x!bTyrL(Hbf}_I)e1RJM`f=`eUe8qUBAAp*rq^*bdV=^0i!|I zs_PxR>TacIm|%o7DWRA3Ur1-=z5I-em$%wDn2!Bn&f2fa^-@9($ALcQR5>^-kt?pX z*LTOu7+1-qny}3z(pl!1V7WK0@~r_d6R4nGr6^PxeI@Z%Lk!}TpSwGTW6H&XZ4Ix9 zIYKC>)pdVWfE;-G8Y}U=K6|L%p3(&*1~U37?`Yh`L@BL|LM!Tx3>wNiRk)hHD>8aZ zHtl6-z+A;SLMJD9nu1)t(?nTSpCzSFxX{Tm@c!Oj2_{h^x?ux33GJw?9h&+RF{ z5u)%DFad=Dt#RSf>%8}85PgzhWkp4%gtlcV>uT0Hzs;N@Wr*VMp!1NF@T(*ruA|Rbo&|(2 zVHrlzxr9?}uL5ozyezrd`=9fSsLMI01sS9nyK6x%M!cT1B?Qp8wt72;kSqkFHO0u? ziG7T)sPBNUAO@Scv0!s+#NXFUpS*sZ>Fz^HzH7{@`OlTq)K-D{#dQLQxy+W*qDAnE z;@*Ca>-RSu4gYH24^yZ=kgKR<3lZMc{JIl!*H^E9x!sFLXo=B#JHvu}Q!eZ@yfLX+ zJ#vIJG8Nf5vm}-EQ*1PQ3G=e+wywS|+GhkgsGH_;qE}sv7k&Blm|^WT01crmhIG<} z$d&6jwnB>$Mfa%?(n>nJOs5kNevV&GGc2>HdogSxKpqo4SOgC8ix3!PF6wihzFqg# zLuCiXA{uM8A<1)N*O{0UfzYNTAj2+=MM*K;jVH8Acsc2fs9EJVpD9^L*_!Mpdoq0| zQOc>-B1!4_+0~!7WLBD61~{(|{YoAXjTpGiA#y%kS0)vr)=Rr|Z>Zrq%Z`(56s_OX z`px%l$S4#}Gtep4^}b$Bck%}2$k^;X>E-H2=v}XSkU|b-|2N_C&y3y_e{jGm-7x+Q zoN(3+x|5Ix(a9e3FZ*HvF9b!WZW@9;>leC>4Wc*K_-AMXzQmj5SR|p9*?2CkE%J`e z{9KIfZ_Vh+$<)&;Wag07L509R)(uVs1r!$roUCZ%oyvg>9r?u@k%mUm&ST+1VJHh- z-J7dgii0yF4-mP5*mo|C47(zVN1dm7nB zb7ZegR5+BE?`RF1CzJP=uUa|zovbPcL8OHw!Cjs_qrDCteWFW~4xbQ~*Ny4Rt9|W$ zq=z2Ru~*`|Ky%{Qel$W~*>=jddOh6#6rt>93UMb8y% zSAFIO(LX1tSQgD$8M9$I*{6o1R>Eh&oL*vTZY!*^^i5Sg6|>)ui=CB08E$d^b0 z++udQ9l9x*ugmb2)gM_BbJ4l8v_v2_IkS@e&@!BtYe{0&W_p9Z`|5+gxW~q^a(((a zus%Dmm+1z*U0p_yTSKWv>qn1zd4nPQ{^gjeQ7!ovciWuv1a27%iF%<(DGzAU^;9iZ^;+6j$_NhJ zZ8JiOzw>F9gDs>Zit8a2Urk!rzIs>$v;WQm?W`z1AzjT*3UiPrqE^|T;trp;Cq{>ShJw*9Mv3{c-_jR4dYYzfOZAC^7F&9jQr9e4t%j^n`G zZ8|{v-Q8Xiab%ddDo4WpI3G+{LICm5!o5K1I-h3=J_Kd8*v*uf`{bB=7lUsQKz{f)w?z=V@ zmJ&SH!K8Jc=?e#ZaKY3rXh);vqtb0rNyq2I)*J|UP^r!5Cp5g-S?`jYo{3G+uBZ$hpmEHK4wpOU=5{JY*`Wh$IQ9?FPz8+ z61C2^dmyWp=%PkO|E*=rnRJ@}qg3}sB56nRR@jXjILeIvSuOUnL$c+@h0rg!rChh~ zADiV>`Tjz+OHlO3gYZA>^jXVkWwk=gQRgJ%A&%bKy|M;R8eL60;>nJ#^+`Ow&Wbbo1H%m%@ z`QwrniBeO#_fh0u6Z>2cV2p@dbmU&mh?0F@F|Cnn7XJr*< zMUTpHkWCnWzwGP(EYpGIw>J`p}#{Y#1=ZQ9xe{q)n)T(;@rLpj*(5&{##!SYrw&f!E zvv*f#`20U<67=GkO%*%O0eO`;cd()d_w&mcYzGy{A|?wb2TGlK7iNqH=?>-+4?wUI zemUlB1J3NSbqI_^CVhKC>CgKLH{mMP`UPpy9yvPmI{3(P*U~?#B-CriPoJ^Wc5yNTa@KE82%9}0Hiz17%p(g#xHf<4_>1os&%R+s)Nzl20e6fp@ue_G zWD-}I6R1Jf8#1iBA~K2Vp`QU|?`Qw4t>ef)K5QPmVR4-iM zv6F%HHA{PJWB>TEf_|;7(CSF?-H(~&_&j=m&B3^2t|F_`zKHkyO^!ODImjKti&xS}RajXV$qo7Q)dCd34($%kd@g-quygha$^2a(zq&T9Y0W8;J#S z!nmsP0~uE^yVwjR-2P>nRMQz+uB}@(=EL`VCa>k%2~l2U0Xeox(c-?uiF7;ZtWQvL z1&5*ES_zi&NSQFwXj|9NFDL)^<2rdALS06s!}uce_B9obJ+_}R$jA=y&fath|M|VS}0@kPpy6(wff^xH|ZZunIHN#gBFD{ z40>cQgF=CfaGs)&#ja3zUd)IiQtab$@ee_A|crs;qOh%?; zBp|z2>Y;8Gj+&P`qd|v5D8(fYOw5rO830{f5H&$)QyNpKN(#$7PO&N()rkV9w>G{$ z55sX6REMR6eAmYo+FMv00VSBFT+{r1O~;Sn_4r4(fP3iBccYkRM<6wTmw04iUS;Tv zki9$6;xB!aESvt5i-w*HgdW%QUXALF`$;MMt%})^ysevzDw@y%xz?u+QX*hZ`@QTQ z7U9_h1#t(?kF)81D<3l-YJxts<`Il=QU61Ctu)HblFap#PrhPEt6Lk2_xTbZ_^%RI z>c?zWMtm2oQc0c+BNeTDoVReX8w>UJEl+0-^c%Zp`ZHRCrP=d0muU?7NVF+noyq** z-BZE+eUJ4R^{_3P3WBmR~VNnmG`Kda%0~x zcYRnaKjzKyXufmb5swM_s0{bE`i#PX&Z`$daT$RZD*4IeV|;_6uM&IQ_)Ak#c2K;du(^C!FyvO7Il!@oTXYWA~K}qA2wk*$pZoO zRJa!r@F7BA^~23+iMz6t%@sZC(_E>VmuKkn_qRbY?uCQWUjC7 zJMWRw?j59k-eyr&{$1m!1cpv61}e|fiq_S5mNElXm95>^;k=6AXiiawD6#yiTaZiE zX1>R-iWvTnSKsA&cnB1?cf2JdR*D?3{;#5M^a4qg=(zMa!CS3IYd*23tB#&|1)q5d z6GEeh0J~%T0`}4q{yS(g1W}|CsFZK1%+m^5J#dY1aG6hjBFB$B5zOqtIupsddA_16FFi*qi zwm4NK+Uup;fF42)x&Zl!g=CTcVrL(4l7ui@?{AhBh8J|43W%7p`hVB3J{6vgiS^S? zmH2ke5WJ_{OkKjMdJD7UIA3qd{*+aWD96>~{tthYd7={&{|wBK*MF7dhSaOgMpnLQKJQp|YGz!cVLN!h zbK}kdn1+s1jF>HW^S^)~DItT0wrI^nU-{J7Px{+54tU{2D4-yQ7;w7b5>er}&KC#?588-8oaZLJ z|6=flug0g^TSC^r8Ci?T0&=V)tRwzs@SfA3WWcilZH=$@Pd1Es+aze<2Kq!wxTCDe zE1^(Wxf7H6f6En9GqpNp)p`z?$% zaZUXpOhEM5=hslecxTIk2bcL$_{o=b=8YCp zF03=-)~fgVQSjF915)BD5a^CN(-gA~wXR8kd z$dEx3(INMQit#|)6_kOy=@uHQEVl<&V9xZpy>Guz)65;mtt$f11HU+`k_JbpWk2Se zr{DHTaf=SA<)_cn0<1k3Um1X*(r7jDTY=@o#wmbjH!bP5YLeJkShXvz)!KN9`Gg>q z-KX1Q4)7voM@&bYMjv?pXf`3G6`5)R*-Qt+vnOJ10gOdN1BHgZB&e)YZi+n7RD0$` z`9Gt^zeq{_ z|IB6m-+ZwDv)j>=fsVluTG}2e_x8~rCx0-c!rn3dM!BiY;hgcp^t1v5qaIcNYFpCX zB|l9f(6!r{GjX6+sqWpoQ||YGXRZ_g+Pf$Weqm!)vNB$UM_&CNXHzl+2CAf%bY*67 zz0?p|LUF=#P0Rtf{RDK&3|x*cvN80CHWv7#K}!Tm0xz~cdK+Hw2-k!{``0;3%NOkM zZ19#ti2B#(JDF7uiX{Bkg8uP$Im!OgC<%lvA0>^E3S@98hpw1g~sc|J}=+pSb|RX^v&P z8*%iPOk9L(zK#uxB5y?dI+z4@aZ$;2HHx;# z`iit6J@qvR`|*W+CiDG@5_-^N9%**1`Ctn&m zJ9|@PJo{T~;5B{Skck%b@if$(O7u3c0hYF*!TWMrjbZsuUH6bQzzKGA0*RGcRnuI6 zn9#O_;WL4nQ}aIPfTM59j+gWSECU{v7!Ytz*Spo~qwVAb&@@09K=fG&uu_0x!;#k*p+g)fRL@_QU-%(nQ4k~d zirfsCWK-pGEKr;$kwYI~<~W3rRO5$kc{6V}&K%IoSBS8;;_top{x&b1Os%qqwi6{e z#caQIdvisx$~92Iii}ykb*PvKFdf7l(f6>;qp-oDNFedaKb`vl2)O`{RCIAw)+9Jb;FidUxFou{e*=!0i#BeSeqGwTl7bSg69kOAcm z_EdjT`nO>8k2v6C`&#})m|XGyWV4T`u{WFLP8@c?=mukv2YOuAf8O-?fVL;(Y5tk4 z{X-6{_Wa5A-x0Kr}!2J5%`r3U)F zT1FY_NfK$L&hA+)rAbQLT<-J5%fuXDN+wEpucpEyaw-gBQjQ0Le1=KV<4P1EMFX1f zwiP#02r%G-5JeY(<5CW=D62fNB-P z&#t2u=LTpWLbo4E=^Wkx%6i=h6p$eywsm(q5GMD3l-H4Co)29Am}>j%m6WK6X8Fl= zmUyZONzj*|+AP;Xc(6eYz#iUz5=v@rK3q(#u6+OBOz1K+f6CH+8RuC9$_By{q|wmc zCmMTfS4R)2e61;f(wJ&{ppA~orcN7&PysV7ikcl#Nzkc4QG~#b#f(>)U(OJ-%N8>z zo61j0%;31HV;5qACAzt!FjHZwK40b%AY<4h;R1l?0={#VPk0UNR4)3A zw~_(NJ^btQ(Do1A{J2&hAfBWeD#FhhYn#Q%ZCytqrfKg-N{wcRS#ifXU$Ovo6Y~+* zuzoPLZTK zh+!E(Ts)xXtR+kWSO&nmCV@Js3Vle=!F^h$VUD7hhr`nY^Fp?lwE)&4vE@&v-*w;( zApPsKz}FsVP=)`SpFrTou6x@wV3Tq_dvG+d!Gr-?2hcH4n;mu@(hGzL z0IB)U?oULIfpdfO9(MtWOXOZ2VxhbvB|ie>0$nDaJ$n|&&}XS!V*YQ)qEb{!c{T`aEV46VR4FM%=b3CmrZdiq+VR-eX#TftN_U=$Z~y=4 zu=?LbrN9*bubRP%Dp8!C+vPJasu-w`Xxy%t>|6vo*VrR;s*m)+U~5CFkJD}VPwQD! zF2A<%aYs*=)c>nZZKl|osK}CHY^x01ej-8hMMFX#U(twkQk*Si(Qn+Qikm9FlkY#` z%^KsJDE!NBDvhrK37Lqa!Sj;5~Y1tQSCX)K!YF#d%hwdGCMnD>)uhZ#3H3I$Kda6rEa zqGJcPEy(mKZ2XQgby!4M`A<~`Q+F&?lEK698Nxdzp?v&#;xdbrk9)OVvkWJ20cbyj ztaAUJ6h$V%&2(zs^QAwui>B&B7PtC&jJJtANO`9c6>wKh)yA9i(_ci$O1jA_z|eI% z1Sa5<|EAHEIw)vC?pb3kjbHFl*aM(=oHo9G@Bz?W2$&riKH&O^ex6uU`LpUd^;dg) zQ1JLPPoP7vWH&oEe2)61S?|&^j7uAlbErX9j zcnwM{SW8PFr{MbZSAiSKAx3*?CiAbiLMNO3{?kCp{n$i4cSw+-c6Fqf*d~B9RaR7X zPKdYDE3-8d5)qNaa{;1)6sbL{d{5Z2$CvWOEH7{X!ZMTc;LZpQpqDgnuw#o8(2yRXS4F OPhDC2Y1tFYcmEISt3#du diff --git a/.maestro/enrichedMarkdownText/screenshots/android/inline_image_display.png b/.maestro/enrichedMarkdownText/screenshots/android/inline_image_display.png index 84f2ca5ac693369624437a363b75d429b6a517dc..f8d2f642a3c1b26e3117becfa483212dfa54a123 100644 GIT binary patch literal 14625 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 BCXN6A literal 17261 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 Q;d%xsaryUU?+m~GA2WjP%>V!Z diff --git a/packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt b/packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt index ed13aba26..dfd3545f6 100644 --- a/packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +++ b/packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt @@ -29,6 +29,8 @@ import com.swmansion.enriched.markdown.utils.text.ImageDownloader import com.swmansion.enriched.markdown.utils.text.LocalImageLoader import java.lang.ref.WeakReference import java.util.concurrent.Executors +import kotlin.math.ceil +import kotlin.math.floor import kotlin.math.roundToInt import android.text.style.ImageSpan as AndroidImageSpan import android.text.style.LineHeightSpan as AndroidLineHeightSpan @@ -186,7 +188,24 @@ class ImageSpan( start: Int, end: Int, fm: Paint.FontMetricsInt?, - ): Int = getDrawable().bounds.right + ): Int { + if (isInline && fm != null) { + // The line grows to fit a tall image, as iOS grows it for an attachment. + paint.getFontMetricsInt(fm) + val imageTop = inlineImageTop(paint) + fm.ascent = minOf(fm.ascent, floor(imageTop).toInt()) + fm.top = minOf(fm.top, fm.ascent) + fm.descent = maxOf(fm.descent, ceil(imageTop + height).toInt()) + fm.bottom = maxOf(fm.bottom, fm.descent) + } + return getDrawable().bounds.right + } + + /** The inline image's top relative to the baseline: centered on the cap height, as iOS places inline attachments. */ + private fun inlineImageTop(paint: Paint): Float { + paint.getTextBounds("H", 0, 1, capBounds) + return -(capBounds.height() + height) / 2f + } override fun chooseHeight( text: CharSequence?, @@ -223,10 +242,7 @@ class ImageSpan( val drawable = getDrawable() canvas.withSave { if (isInline) { - // Centered on the cap height of the surrounding text, as iOS places inline attachments. - val imageHeight = drawable.bounds.height() - paint.getTextBounds("H", 0, 1, capBounds) - translate(x, y - (capBounds.height() + imageHeight) / 2f) + translate(x, y + inlineImageTop(paint)) } else { translate(x, top.toFloat()) } diff --git a/packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/InlineImagePlacementTest.kt b/packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/InlineImagePlacementTest.kt index 4bc29d3d9..34dd0237c 100644 --- a/packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/InlineImagePlacementTest.kt +++ b/packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/InlineImagePlacementTest.kt @@ -5,12 +5,15 @@ import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.graphics.Rect +import android.text.Layout import android.util.TypedValue import android.view.View import android.widget.TextView import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.swmansion.enriched.markdown.spans.registerWithSpans +import com.swmansion.enriched.markdown.styles.InlineImageStyle +import com.swmansion.enriched.markdown.styles.StyleConfig import com.swmansion.enriched.markdown.test.MarkdownRenderTestSupport import com.swmansion.enriched.markdown.test.MarkdownRenderTestSupport.render import com.swmansion.enriched.markdown.test.TestAstFactory.document @@ -19,6 +22,7 @@ import com.swmansion.enriched.markdown.test.TestAstFactory.paragraph import com.swmansion.enriched.markdown.test.TestAstFactory.text import com.swmansion.enriched.markdown.utils.text.ImageCache import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config @@ -35,9 +39,37 @@ class InlineImagePlacementTest { // the baseline, so an icon taller than the capitals stood above the text. @Test fun anInlineImageIsCenteredOnTheCapHeight() { + val placement = placeInlineImage(MarkdownRenderTestSupport.defaultStyle) + + assertEquals(placement.capCenter, placement.imageCenter, 1f) + } + + // iOS grows the line for an attachment taller than the text; Android kept the text's line + // height, so the view clipped the image. + @Test + fun aTallInlineImageGrowsItsLine() { + val defaultStyle = MarkdownRenderTestSupport.defaultStyle + val size = defaultStyle.paragraphStyle.fontSize * 3 + val placement = placeInlineImage(MarkdownRenderTestSupport.styleWithInlineImage(InlineImageStyle(size))) + + assertEquals(size.toInt(), placement.imageRows.count()) + assertTrue(placement.imageRows.first >= placement.layout.getLineTop(0)) + assertTrue(placement.imageRows.last < placement.layout.getLineBottom(0)) + assertEquals(placement.capCenter, placement.imageCenter, 1f) + } + + private class Placement( + val layout: Layout, + val imageRows: IntRange, + val capCenter: Float, + ) { + val imageCenter: Float get() = (imageRows.first + imageRows.last + 1) / 2f + } + + /** Draws a red inline image between text and finds the rows it painted. */ + private fun placeInlineImage(style: StyleConfig): Placement { val url = "test://inline-image-placement" ImageCache.putOriginal(url, Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888).apply { eraseColor(Color.RED) }) - val style = MarkdownRenderTestSupport.defaultStyle val rendered = render(document(paragraph(text("Inline "), image(url), text(" image"))), style) val textView = TextView(context) textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, style.paragraphStyle.fontSize) @@ -59,11 +91,10 @@ class InlineImagePlacementTest { Color.alpha(pixel) > 0 && Color.red(pixel) > 128 && Color.green(pixel) < 64 } } - val imageCenter = (redRows.first() + redRows.last() + 1) / 2f val capBounds = Rect() textView.paint.getTextBounds("H", 0, 1, capBounds) val capCenter = layout.getLineBaseline(0) - capBounds.height() / 2f - assertEquals(capCenter, imageCenter, 1f) + return Placement(layout, redRows.first()..redRows.last(), capCenter) } } diff --git a/packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/test/MarkdownRenderTestSupport.kt b/packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/test/MarkdownRenderTestSupport.kt index c68628907..8298776a7 100644 --- a/packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/test/MarkdownRenderTestSupport.kt +++ b/packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/test/MarkdownRenderTestSupport.kt @@ -15,6 +15,7 @@ import com.swmansion.enriched.markdown.styles.CodeBlockStyle import com.swmansion.enriched.markdown.styles.CodeStyle import com.swmansion.enriched.markdown.styles.HeadingStyle import com.swmansion.enriched.markdown.styles.HighlightStyle +import com.swmansion.enriched.markdown.styles.InlineImageStyle import com.swmansion.enriched.markdown.styles.LinkStyle import com.swmansion.enriched.markdown.styles.ListStyle import com.swmansion.enriched.markdown.styles.ParagraphStyle @@ -90,6 +91,9 @@ object MarkdownRenderTestSupport { /** [defaultStyle] with only its [SpoilerStyle] replaced. */ fun styleWithSpoiler(spoilerStyle: SpoilerStyle): StyleConfig = copyOfDefault(spoilerStyle = spoilerStyle) + /** [defaultStyle] with only its [InlineImageStyle] replaced. */ + fun styleWithInlineImage(inlineImageStyle: InlineImageStyle): StyleConfig = copyOfDefault(inlineImageStyle = inlineImageStyle) + /** [defaultStyle] with only its [LinkStyle] replaced. */ fun styleWithLink(linkStyle: LinkStyle): StyleConfig = copyOfDefault(linkStyle = linkStyle) @@ -125,6 +129,7 @@ object MarkdownRenderTestSupport { codeBlockStyle: CodeBlockStyle? = null, spoilerStyle: SpoilerStyle? = null, linkStyle: LinkStyle? = null, + inlineImageStyle: InlineImageStyle? = null, paragraphStyle: ParagraphStyle? = null, headingStyles: Array? = null, ): StyleConfig { @@ -143,7 +148,7 @@ object MarkdownRenderTestSupport { subscriptStyle = base.subscriptStyle, codeStyle = codeStyle ?: base.codeStyle, imageStyle = base.imageStyle, - inlineImageStyle = base.inlineImageStyle, + inlineImageStyle = inlineImageStyle ?: base.inlineImageStyle, blockquoteStyle = blockquoteStyle ?: base.blockquoteStyle, listStyle = listStyle ?: base.listStyle, taskListStyle = taskListStyle ?: base.taskListStyle, diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt index 04bc9bf07..03c1f5b77 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt @@ -28,6 +28,8 @@ import com.swmansion.enriched.markdown.utils.text.ImageDownloader import com.swmansion.enriched.markdown.utils.text.LocalImageLoader import java.lang.ref.WeakReference import java.util.concurrent.Executors +import kotlin.math.ceil +import kotlin.math.floor import android.text.style.ImageSpan as AndroidImageSpan import android.text.style.LineHeightSpan as AndroidLineHeightSpan @@ -282,7 +284,24 @@ class ImageSpan( start: Int, end: Int, fm: Paint.FontMetricsInt?, - ): Int = getDrawable().bounds.right + ): Int { + if (isInline && fm != null) { + // The line grows to fit a tall image, as iOS grows it for an attachment. + paint.getFontMetricsInt(fm) + val imageTop = inlineImageTop(paint) + fm.ascent = minOf(fm.ascent, floor(imageTop).toInt()) + fm.top = minOf(fm.top, fm.ascent) + fm.descent = maxOf(fm.descent, ceil(imageTop + height).toInt()) + fm.bottom = maxOf(fm.bottom, fm.descent) + } + return getDrawable().bounds.right + } + + /** The inline image's top relative to the baseline: centered on the cap height, as iOS places inline attachments. */ + private fun inlineImageTop(paint: Paint): Float { + paint.getTextBounds("H", 0, 1, capBounds) + return -(capBounds.height() + height) / 2f + } override fun chooseHeight( text: CharSequence?, @@ -315,10 +334,7 @@ class ImageSpan( val drawable = getDrawable() canvas.withSave { if (isInline) { - // Centered on the cap height of the surrounding text, as iOS places inline attachments. - val imageHeight = drawable.bounds.height() - paint.getTextBounds("H", 0, 1, capBounds) - translate(x, y - (capBounds.height() + imageHeight) / 2f) + translate(x, y + inlineImageTop(paint)) } else { translate(x, top.toFloat()) }