From da59fe24d46bc3ee0d77cffc4a1f998fa0e12e12 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Wed, 4 Mar 2026 07:00:14 +0900 Subject: [PATCH] anusvara positioning fixed for DirectWrite and CoreText but now broken for HarfBuzz --- OTFbuild/calligra_font_tests.odt | Bin 17302 -> 17283 bytes OTFbuild/opentype_features.py | 263 ++++++++++++++++++++----------- 2 files changed, 170 insertions(+), 93 deletions(-) diff --git a/OTFbuild/calligra_font_tests.odt b/OTFbuild/calligra_font_tests.odt index 0ab6a8caf3f55d01512325886e974e715dd25221..df8bea4ec98a0463ac847bb7cd142390d08c1ed5 100644 GIT binary patch delta 16040 zcmZv@V{o8B(>59>8*RAB#YNY-~H(*mknn=-cP{&N*+rRj2=S%~a3S zR99Ee%+-CRhJgl!fg&hKgF~Q!{70axj1v%~!2hWRt$i@ypVl$%ez2p&0RdU@1p$Hk zXJTw`=VEH-Lg!&?bHr<@j4heE^QLZ6FTTbI1K-PQEAR1}dGn;XXp0-QX9t!#DS}5n$5q^?9xJ@l1aIys2f~9Ap1cHhEs3S65!ar6x%8>=m|dWzVC3H>>|S?^3$& zj(*AWZRkFjBw*WhfkVx!KY!kw=L|zvb&;)W(b_)KaWTuO zMdPM-k&X8kr*+sVlh&?aX#-77AA`cOo-I`?3lO}x=%QWyU9wVIAKEFDQ*&rXT;JQe zPN_rN!W06Ta9Y*IK}!dM^qZn%Vea$-QA6{;P{?oPEF5i4N1%BQ0hkx94a0gIFmwtx+l3dgk{lfe|It=Q7c`xpplZwHLlQ0v|%G0zZ5Xh_+43 z8f#bWWE@_u;qMpOK+T46O-!m<8NyHF+Pc;?8WoD#^UO-Dl%iD|wYODni@S9znws`o zyglLYxpOxK({lC#&i4H9?pYe}o8S`4oD=Ha>I#KzYzT#bixcW{xYE-lDVD({U>|~# zYfCLEiiJa9&@k9Z^9vVgDZ7%(?7ePSw_rf0MFTrafW*y#Ca|Uu%d%nxxry7c#a{CF z*#;Ae`@61F>?tFcjv5~ln+iTaJ+nF%LEgZ_kkO3>aL>As6*v7f;L?*rN4 zGPeyPrukQGLBN8tBI0_EFLq}>F#b0*v~f<=`7-9{HTAmaIEIJbeEiJoFCa<7$q=}+ zf{hV-LN}fE`ZGnJ)(fp!!u@Trw_D zotY)h=qBWE3D5B(t3;5nq4{piK(+goI&e$0RwH{Q)c2+JR~w#)`t$tf-PiJOy0Fh2 z3U2Z9>>qp9$~h(dd(Cs5F{zR6Q{_&b8D2+W1*5r=ILE`s>Q3 z<&v_r$C67&`j@F+%np>HUnT6KjcdzV@C6xb-ji09O_Pst10_+Fb;~-9U0?`}EXHAe z6gtg!LqP<`5*c!bP~p*DpGTj2Loik$Mh%&1L}nxt)bo5y(zU5|pc12Ul9q$eipqwj zu5f|KjE%#FZB#XCT?LvcU-h?;Zb&SNN606amWy)X0o0(?{Jf#)2dBOvQ*0QlMANQK z^MqJzl*H80=L4Wk`1ov=Z5)WM;SXeTo zMxp6_^#eLW;B<)=@C~e)fuqM$IM_1)Jy7K;T9wRZnWpXM7xgL z<1uS$y7!ZAq3o)^foQskM!ZTagu^@{k)d~_uoaXtvX-76d6VAqhkeZD;H8?zFqR;U z`sCt#^*+RFA!7ZOq5`WThtQcfIgI1ylw)Hn^aXgI_Y_q z@uYDEhUD$Z;q?`%dr|5h&UJ)q{QYIESUlmfZU}rn50Z85#GaaZc8@=Zy?KBYE4!AA zenORjGQ?8Fc)qB9CqHEt*tZn_Pic3~JHX(x?7o0si|O&3D*Q4AqF$grR}Ci2nW#s} z*+SR(AJCt_017N84aYn#@hMm_IJgmps=j*vjtD_D1t>r5-%_U)2OJ{A!FJV&br4R6 z_i5Mk0&k}W)RiHosKfnTJ4q@(n7JE%E96J}bnZtmA-a`O(fQ20zKy%?o?{Hye;(1} zf0dfJ33mTINen1x*-4J**c0ld)bz-szov8(_BJDb0UF6^BeKiN;&Es(Zldf3V5qCZ z&LoHVEV!zogO7gByAEa=IBRFSg}e7P#8cy=c+HFmKNaNnWmUL}hsiSWj|HoOfEa z!am8R1J}!4QZLJ+6BuB-x198%Jy6(UEAYk;xFz$%?bk?Nnep=vBJ1yDUULPeHD;62 zGLXW51dNuDxrVS_l7iYi_!2O8`d*ioR1QZm&}IWJmPozIA2YjD>l663bpx9eJ;-5I zwP8<6Ey@HLxJ)CIXe0lYoYI_Y<8TOjtjK860@rYR)`AWLzw~Fu2afIuzt;S8+cicq zm?WrFG0RB*JpRF>U2&RcE{8Nl|4l0H95w9}!*@~b#)EQ3BrbKLd&S53(Qm%eSf9Zd zGu-CbiCJLm=8rMzpJhM_Jv%oGaiTgg{e23oK@%I&y=oE6=KYrq+X2a{jom;Q>dor< z2cZ9g+n2nLVMKIgB|{=b<)tr6y0ny?YBb^YD;QJvMAW>BNl zrYN6%hs|m?r5gtA@LeKxS~hLYKPb8hg**KekryoUqgE3yP(`zjkor`B%61S;j-1dH zOBvy@VFiP9O4&n$|AtPz1XXeA#!e)81AvIGSeALNq`YNSsV=+a;Y`QWq9(9Kwd_VqW*UWpuiNV}H!V1}b?-aS%hvmBKx3am@sbX_ZsB4){p9^3C7zfFP#PHNJ8>8cH9bxpsYr4U zRX6*SIVAFt10|jaNXcqk^NwmfKwK0%lbKBN(o7)h>UO&yRE+W}vu6~#gfkssUT9?xl3(ckS#`!BN=J9%q5|dFyjB=P3njhN$MEfTiXl{k}zZ_sgt0_RX<}p z1^K9(*gix+;wXcHL3&4c>~8cI5A3@nH6aPPzdL@}Bz;6uq6f6XPD#0ClcA&!NyLH1 zoSb!JkHjj$0{TcZ#zh=4Ab=7jMyc6sXki8`ZS4O@R+`_Bd4qN^29BG`CW)4S+myH# z^y*(2#LgD`(GD8#Uz=5w)t@W$SG!zcBqSy7lELO1+oGu#$#EQmWPQSSNN?k+8hB&j z*6AdK?a5ZiQ(hLK~{Iq>n)!g85=i0X*Sf{Y0lZ>A@If{z&qad0u0k*9S3`+ z^|h&=vGMHf2SLLI=I*Ijwjk{CaxQ|4Ie1i-u3{s)KlSfVX^()ynO0W;KQTf(-8FDu&Gtb_mEq~e=9ikHsrSDNbA=})8`gbd9O zUnDTT)*?PRx9@Z(KMA#Wyv8pMnUd{f0~w4OYLLILXG_|Q{LtgaBCb2hQz$05stI6N z_kqYrnC?A2?$K^OrvI3)`GlB5F1-In#^$}rHY(QM9XJUa0<7;=XF{e_i1~9x+qVEw zTufjS^Q`Y&^I~k?&}^H9Dx_>TuHYui{aG7fkIEY9{2L)mFh{F|^0%u6Z0$a%4z7bw zn6)MPVcoZnLb>=0z1p|$h`UgG=Yd?{w|sC%MYq=hCrCQC*B4=vaHik)0=e+~pAZI_ z;a9=Oz&!vEU;!>ZFSrZ_zR;{CdM87L^Ew;i6-QuyY^DXSO7idZwMkek$ zJA36$sJb~7olRNcrsa+sRY%VCVBg-=l5x9w;+4yZo0S(g?r*v2MHE;LnO0?E7yv&Y zD0tPpBOWi9`!F)x68XlM1>?fXw&x*s!WPacUmei{IQqr#88aC`*RGDjiIlpjb|{dC zb36ZU2f4#mk`VhW@m;>cC3Po4UMTnS&DeCXN4d!n4c(fWptc<98n=glECdESy2V$Q$yY)i!Dgyqp3+pZs_8waW)|H# zl6I3LVIE4Jt0TsQKFGWp9`ARW4|aZ?esgDFHW7^uSdXg>{W|Dt;riW2+xauz_OcWb&!BxB(%P-$qNo`b6g-fG zX%yZ_Cv(5gDMp}MP%*Tq>QW+h$j#!hPnGY8&RG8;3X6Nx4MXkPBB#zY;hXGa6KM(< z7)y=f5gj&4xrRG}Rr36LBe9QNut8$$w>vAA)n6P7pb*9|JH7WLKhF7m!Q=vlZ5Gv& zG*Lif&=$aP+7ENjQ~V@s%8KNoF3TK1qOx1>$fy^9;o6;dz*TaZMi%h1+EM)i<6<7i z`^?lAeBdKF=dv8i7+N*Pu@@xFCs+(ndoEKMBca2{$Uc~|thFIh0+I&UjBoLS`air3=8KBxdu zxeBA`h_5)pW2I%djEOeC`>GHbwCpw3xCUF2z&SN%OvXBl$yPKloj=3K6O|5JEOSDM zl2vZcvypdDQG~z>2H9L8pJi_dsu+rHDu{xWBYr@c&RajjD<}Kw@gAJR*7fsHKX{bi zEP0sX9eMI1GKc7?w8T}I^31aR3>91fomLoFi%2d&B@|`msDNrK(lAvqcQ;(7&>)@| zN2u@|jpM4~t)-VN>0waGPNoXCPabJ}8`I zh=}DBE_+uL1qxq8<(dJ<=88Up6Ke$%DDAo>LpEF`a)e_1=9=yPl+}?a_^wB)xFVi% z$h!XLhJpcNDGLRpIBd(#cL$z&Rs)zGmusV>EbYN=13HmV)KMPeUB7LPm&suFy9DWk zSgBO1#Sm^S;*pOU!p+Gspv(eh@`IvmMO{5ZJ<8J9utc;pk&pp0tNH+4+>&k<9OuHA z5rQ?y9S~pd@))!dI0e$eJZIiWrA*@AY{`ZZ+8kFC;H1Hfh&+&&FxWb$@R7n#``#88 zL}iV6;u?M{&#;S4e&?h9b*$!&1TPM+)Im91PM*<0qDbV>dOhP19t1@9?K~w=dzE=w zG)p4Mo7sHH@W_B}xty$ehGIk8x7weOLKJ*mi{IO{rp9mSF>Eu&HM1ryjH?1Gz_FPb zGM?fQJ}bY8yebrXj(Kj4RpjwKF-|>HqC7F#LZf|er)HG{4O1m^ad0x{2r6|mAeomJ z-*|{t9d2teiH#YyetWz@NTZ97JXXNR&MDZ)d7*=Oqn3zxm%F4#PaBaURSn{1kXl?w%e@xju~uF4V)S$4kc&9#?6i;Qw!sqSW6TZy@Z)g(~;#H6-wb_5|!k8htM$!(Gd#k z7e}VRTvs9o)+(NjZgo!JU*V?w(OdA3miU2%pCtS^5H~$uAFO!m5oJ_Q=w?e|W(p** znLVnxp2~L)6``YIO^zJ06md6|f9E~jf@(XvhY&k4iYkFdy%6ja zLcNyfBhdXN4*7d3itjpyaV?tAlJW-3t(NagMeSj&ii&9^k7(sl{m4UmSP983d_HV2 zrcviEZeOgJBJ zRByGGU^0X7NG;<*9GWYGkuA1-K5N|zHk*2l`x&g6tc=M~_5`L(T<_3e+$aNRb3}M| zvDIp4=Ket7YAM%_`GmhVqd(C$$%a4oNTahGN?T?K=HlYVfP)K!Ud9oQPCRr3bu}CZ zi_%jmo`x&os3aKivg@k`hJkbp>6`o#L7+hu6loM~u*)>YID?`0B-Y6QjE&eQf#$=XL z(P@9YBR_lzoD>3BFLuR?V;qb=_JS{SQhIV4+hm{Rx;++N#^D;AdHMb@vC?`EHr8oB z$4>OVH3F*7-^V`JpH6p24%kIw}V)bCP@b)>lcr z%S>*vFYkI`r>p77*?YUX{F=N!^j0S~zuiN3y^e{3#PlKI3xz!4-FSH;QE0{q9pSBW zKQsZbPAlFgY+hmcftOu9^H(W7itC%=cYp1O`vunz2DAT6ax2%FT?=~GK|vMh)2sbu z#|s%x@H&F)JXXuv+%N)~Ru05n#K-@^w2f`j&#pGnL-{+^FUcypw!s-yoNq$iZ%+N- zL$*T$7yY{Gm~ee5(H7*{5w}(oVPh6EOB+um~~jdod?csFOJP747~uD zqI|>DQ_4~lxgi>a!@*tEx6|V(vA5|M@4;|yyjRQ;qkE62NHE|R8(wq`^!qVjC z!h_}2fu#AJSJ7*)>tkOs6E=|e)$?$>lR`-mk<GErrRq$%|bW@_9x+c*0x#i8h`>A=ou6Du~?vV~~4 zsEfgIKTg?$V@7Q+Dl${*%i<2d?al%@H(RYs}VfX}qSv@KjB zHAccTFGKSH@`p3~=d=jZe3`LpJm`04v*UPAx4ZS46Lvtx%znxQgXi%^?5-&MCNkID zc$PSg6&djR<8vr{b+vx_;GK7&2b~Me&!u9O_uVA$u!DV3E_aJ$HWGgWP;W;2OwDbq z5?IRfH->wd_^}Y&c~pioF=tQxbc>*cJ0g6e>ymO+UDw!?t2%&Kgz|0)`4`jpyI1yM ze1z`a$!IdJQL88g6?t*;hZBG|I{La|e>Z=d(+F_Y;1MV(yTEl3EZa(4efBSQo&UNB zPTLJ*F*Z!q|!_#X?| zZ}Fb(hsp~Zch*O8`KJz+^S(;8#cOE~w+ph5c^QLI<-It3)37moro!>O8|y{V1z!pk z?~Jbp?{RT{I)5A!-dUa(Kk`O^$WN7b4BBl?mfgOLr=5wv_U0eHnqN89{Mk=Cq<|&> ztb9>iDX$Ej`lWH@>7m80`T{O&}1BAax4J0v(T&Zb4hoD)8E4#nu4>avSf`vy&$+B8Cc zI~q-^BQ{59>MeH1G(ONtcChk>IC69J1aCkUNl$4RaNsS3Wz(I(+`4Nr2>-Ul% zI3PC8Of&u-#&9og&19xZCHsExks2N@;XEkry>w>~a_oMzB-AoP6i^XuIBg zV|S;Cl<-d@gloi&4M~eMk5q;ZKXA!#ul*f8DjlknNN3y(44x@LNT=?$lW3ivmKde9 zUDgYK{Lea#IjihpkUp#{Gf77;#sF^QhZOLr~yI3L=_trLwfQ&jl{5DDn60Z>2 zge_K95yb>|C7Kb!3T}hkwvzwP4EccjpV_kIqJPp*rfY2Nyfl6*S(-r(*NmI>R831L z1Q(W3*bv}$WEUcl+S|8tBmTA?eiy^9Nn`i%GFRc(1C&F2+FYH2Jb|@J|5`$5i%P%3 zl&5?mGQ)%#9@=m~@ji4ui&eN-2U#Hy_opDmjS923eo*z}6emE&FZ*v`(QXp-46ePyc@>~1KTR$eX1EZHGH;9-!jA_9$vYw- zcHatbnS6p;762xwIXrsy zC!lzMwo?c!RAe?6(b47Cokg=qw{i^-sn> zGEFxIJ)uh!t(a=$aG+Xu<`YOJ6$V`yvdzm0zOR1JrAZmM9&#HUDKO?n#|3W+%Nu}0 zG44J#N)WsFF3G{3a`HluAtEi-MY`-P38*yEa6q+FwbC%-yAD8VX>d@AWrK9n-MI3! zEZ~!5Yk`|0zSi5lBbXD%iNbNIv--V0t@i4HzPr_$&|ZQ;b1lS|l!-4TY7_w1IyTeTJ0Um8`%dxKg|GaSom`~vr^4gwRmh5f zaKTz)8ux_QMWvymkZBaCsvEC=3pUkTj46-Ul8`CFVTSZin>1Pvt8$EMD#V|?zUqb= zpi8xd#Bbj_sw^h?($-Y{AwWm&eKsroIK=WS?`hP;g-cUx=P!Bto4ez3!oLc#{GcN(|=`en| zjRKA)D!7YZy=s+(T`7u9Z7)}2zqpt1F;UIRte*7-s^HD#Qq*0uS)QIN*FmtT!4OCQ zpKZaBD|qp;SjsYjD=Uo4`&tsj)@b*p3S%pq&UZgaB34`gA)B++ z&y~eO^4nE!rYf^;;R%UWr~}?xAOsH&%ncymJ-e44Kc4GpPs-W z!zG{z-6^Ge4mR{q2e;VIlG4DxZK#$XLX_$^;89q}jJqPjU`5VBrsKmBE+6rq#1 z{oMQqV-u8Nzapzxv{LJ6L0f2vaw1H#8*9R@emcE$b4jCGFFBBjNrGtj=;r^*rTS38oXJYPPVV-v+CYWqg8rn`Mi8SwW(MhGJ`uB<4H>JI2iA}kMRVhJV~=cmkean zpj_3HZRb&DwLWu@F|Hzs*)NXWUWVYPAz~_yT-nI5$SR#cdMh!}3SO-n{^Im5#|oef z3kPizW~m_?6c~7F8qvL#afF7p@1>{5&n}W-czqJwRRwgz9{LbOV7)@?%V{;o_cZeZ zO(@?(d5KG64hAE2zTe*;1es>;`V(}T@eJWaQwhYUU^ihN8-pB~+K>mIys+h9mRCtb z97c5`S00FPG|tcGjbl7HQ*QlsLD26*?l==eXUYwvWQ@TeP+KST>k_k_oF&fkmOM6FY1FviE6p|2nq+-qMhik{>w+5X68c^1=ye z^nFB52^nszP)S|@(O{i_De^To8g-#brN=;C(ZQchQ}uqp?0DLT2IEcJx{$hEPMgL! z)65ZqqGn+&`L4iE`rbpo2G*O$>YI+HrzNQGLLkPxztQN8(ocOHCu*)OM>JZO-_mxo z{>%ekot_k(z|W#j>9}offcxk@BYQePD)p}G#awRebzjCxS2&r(z(D34MwT+8hrQ^o zdF`!N{>>HS)UJ(KFs4eCjrGuI5#lPNkn@wc^o zKGj&GZX=6TU5I3H5D3m~aO)`LsirQ2LXf{K5r1(&Z6QrwIJx1uw)%kZFw=9Q$%Wx^ zv{UPz3-&+oFcGIRv`UtYY%*`7^ z2mlrut<81P;&wItCTu*yp>x&ZQZ@5WHn-2a#9gP5QvcwC1K)nsu?2flr)QW}68G*ivi*>Lpm3NHR)>CqO=Lfr#RpV-0qhf(5cIx4b-60}d1N zRd1*nQAB3-Zed|zwR#>-R42d{dO*&7Dst%^5f_JW@j+0O)|ZVhd2FG4RStsjsw>UlZF14K4Q2HM-;X9*%_#EVj8jXr$ZpA=z?oc0haY z?j8^epJrr^u(5>WL5}xMjZwV+NtkI3?XSxCx!1txo0c41ZS1I>8^iRqNIsI7hyqc# z#6XwrU$`hLQ(-~VR~QISr-_L(;G@p+gQz{HrLZv2%5zKI%E95bEN?T3Tz7D=c&z|N zR#w(}&!$xcaE&XX&8j)bFM5FU3hLS9bhTjV{FUFl;ahvvqi$(gfhXVjwPIgw$?n*; zJ`0X7O`aV$6Lf`(d*$OJl1gJOgQBEdVenJ~4u)xIY00?yIE{V3ct3=@Ohh&Qn?zk# zcgrHjeJ_(V$06RqNTgnp1=2c8;^O2>i8^{>#LPSiko(S#0li?SM7z`O>CA0@4ga+_ z7>G|>Z>E2R<57HNFkK6r`mR6I@$CZ_}0U2fLuy=?p(8x$pcOGs2 zs5$(6Hc!%8CY1h<((=>eqn^)4Ws|!H<}~h~%k|~X&Q2@TZKs3TCJpz-mda_W#|g3( ztj~8x4()`l%hQOmw#X`5w2B@%SqLs%;Q}LlptqRwO_l(I_>7N}(eU7iRHqyri6Ekv zpnuqpc5nQyx1})=C~)=kT>mn;aqK(It9@!=T^JP}9Gt+y5($4_QOkHa(HC-r_=%d6 zon1{m*3?pXjw4>Q!oCQu#M33aY1nFP2hHUqt$5PX+R@pW_}Z%DY;8hBNT|8#es7Th zFg7$t2dZXj#bt`{bFnfq53YzcimI3sG$)XK#}|v`*eyR*-?}u6#zi+KxWfN=#>t(u z((OJg79T+CY~jbBUs^#~+NIsGqxYwWFfKejy(S5j(3OTRMoQ3>^z~pg4zj7m?*6G_P!xYgD%joPE03LrB6>3H%CMMk>?mooRVNy2t7FiP7iir4U zwN^LC7Fgjt|AgO$i@&>nRuhQyv9g?JXD5$Px%@+s^49Wqw;2QBgm}8K!~T@I(cM7- zX3YUl42V%=w2n_}v#9ufqi!n3xQu)_$8El;( zE<|oHQd1Ic@9uDIT@_uXyfxT?bkFNAZxH9NkAJQ@ct+Lo-mChVk#Wf~wAmDplqiuI$QfRT36f z=7Tw!*Bf|S*9gyImFP2VlUZ*+gAH7dA2jaWVN0Y&aWETs46VQ;#ln%<%XW)578Y!4 zD~>^0irN*;U%MiElU~k<;&SJJ01AZ>RD#B)CS?}s_m|Ie?A*)mr8_^Se%{4Tson|^ zC0vT`Iaf7kMdms{95EOSDZ`=Qjnu2r)WT=H-8@aY<^N`VA>!T~k~4%K!HqJ5!{T1c7Pq zMMzZrkI6f4?M;J`my=oiIeIGgLF0=@ow_9?vgOi15;e}tMRG|EpsO$t%ndPxlHpu& zU0De;^>fegt&%50pdmabEt8aiJ^79oJ;2-ZEyd-99@7m@lJHMu5;Ny#RG&GaU!&Ky}@>Wuk zL{VuT>HR?=kI}&aXa&O*wd%>m(I))66QV0cg@)1Uwlfp`FI+j;Z6x^4tI{y!dZ1f} zfBhn;D-ZXr=M-0D{i&}r1<9rN_n~aDM)6Ec%Yu-7j4EhtdA*~l3H5C>4qso*ZG906 z;^ue1A_M#)GlS0 zrRU`K)$*f%ENkUAakKXwmIta%_3b=5>E~2oCKlEH7(-*OFJSrAL`+zUh~<|lLxh5h z5Q7*QUsP+Qt`r)T`s}9memkd@s$C5KNrNB#i25%+id(Ig2ZKbUooD4)WGa5Ne_Hq|E5{d~Ho5Y_)x5X*Lhu z@$OEDi77(e?p%fXefayRlbuHjhgS%P65%Nb54VpR5W=67l(RLWNFe&#R%?4vnZkp6 zo64w)n}-LddU|}Bb)iuTq4xv(RdYjj%j$NhJF3SWrPrFwD^N#bCx;Y)mbPZ6!>>ss zZ%A+eX(l@2Pfbltd?svBiUEq4DHTCj+s!s>sDEbX@nwiIJzXG%TO2yjv(mAC*&=hf zZZm%ckO<{)icAJ%XR8zymECY*R)kS2YU|50<3)e=t8Xq9BOh%tS-TodHPsodYYoT5?9*{3onCknVz{@-&)TZd82CpYkNPpjD=8G8P2h?@JJ=)PVngq?>qiMRMB|6Oy~sqMGf76v;7=#cq>;1;&uyQT38ERuw%X=r7sveKe` z%A{|r#~*UQznNAye$AjLmY7=W7!ry*Iyw&Bs%UEdB=c2IxPgWD?A%s*p1FL`1hz^dJ;las|TgETQkFOJ75hfbglIXrykn@C$#*mbP0p~ktrrD?uoDE5qfS6pRn zxmS7mwf)S_$bJ*q)F2LosSWDgVp}(SPYK}hyy8CQc_C1-86vOHcfaIzco)hj6or(48@;; zQu6oqUlkOns;VO1Jv!{Ih<>$uP_Oh4OZSn)Mv?}`tR|;kJQr#j;CkvZ*D&ZaFJ$9jxen&3hoT{o}P-ra#`t_ zp;XQk{>_qijuSCT^;yz*;_wV3ICErI5lT36QzL~AJPFtH zeZ;;wz+pSX_%bQ%Y@UIuT`C-q>&+&AGW#mJ5^3L!k&HrmRlM9YJX;5=TusjYo)V0U zF*5S)vfQ*OCgQJ^bk@+Vkv@|~qUmB>Z!s7u(Q`?xLtx2aJTv!TRYav!f0?qg;_|l4 zx-A2Wk(3RT{UB|X9hSN#nO|g`uo98}Bn)hCdTuKP9i`%~WSrEj-vbn&kDbBP{0x1bmitLDk;-{=Tq&aBkX6i4rQM`GqcIOt_n_<_JdTYm{_ZW zM~oe_qxb?lMNTxhl7oW-!QArAR#Vpci-CdZ)!RL!p_yH+y_J`%k1_wR(*A0Lsc+yP zkY3_uC~YTvEvEp1XklT|)6@ ztrfq#xnmjd;E!7;CML$$=%!|5lG-4z+-CHetn}LNP#|3IKxYqvoQ_NWT3A@vxIKVD zn?x7Vc>vnXVh(5|4NWpMSWDHE6M`aMCdex~T{0!W>Ncz3Y;7D`gA8rE>{FfZ!+FK_kdt7SX_K1#) z&Fekcub;dF-bd7_KJI%mUS6Gn7##F2lKGGr)xec+8M*wx+YfFKm4F$`@H>CFk&#jN zkqBb;pLkbz(4-_}V5MsMeT1pcL+d|{=a3kXGKdk;kayP%Gf5+*cPqNuSKi|M*uTdQG*4=b41dy@~i~c!B#{ zS8sP}>ZX=%3BRbG)rtAS$m`B8>QL9N%wD27vB&3Pz=kZ$6}amCGUq4oPr10Ju3dvRuyy-+1&QEe6`n3o^RS2O%@js>^av z`)W|&qeJ;XMuSMJGYkle$rqaYS(H!vYN4kk5L#yth6R1y20$xwy6#AX#nuMTwpZ|a z2)u2*QXU{+>n!1+O!@>`*Q?N%|*cvd6}3yK^1~evpxy+e{jb z^WF(1(S5vU!ce`;f-Rjz%V+ZR(dzVT+DY=mcVPB*@0RsHr$~7U;|0rjTaAW;@n;Nj zp|;Hmt*=YB-Ec?33d?70VPA_D01B@=msn*egaR~~JZoLt3gU;wIL)gMjg7qmFNjgt z7(+Bzccw7F)b_9i-uxQ^L>|^ah8n}$@bv^7lQyVvvK2CVvvnm(v^G4g^G+uu@p4=1 z@%@9uH7?T+tG_&zGj|p=oE(MCkEtTpt3G0GGVxyTBQAErcv=E^t&YMIdr{apl780*@`3d1o=>e5*?R1VN$O*ef8xOJ{D7zCi^Qx zxOJp?D8f>L@YMd6zVtc^3&7JkIx#)}*T9Mo#!}LG#Z$}Wtb^wxu!Ho3(Z@jYnXSN+ z_vCa)`WANYWa=PiaWtnvT2kD(O@a8Shb0ljROfMnCuysjv!IS)L6HDeGQr^MReFaf zXJ=O#FZ0_m(#$OSHWM???6yC%LOE!C-jksjd7!qIjyKn3DpD|ljY@h6{s(%RvFOpU z-|O20-XOhm%v*24+ZFRT1VF2C;#Kgqv4qYEB+8~%o0mADQ$2pafl^u9IVp+QL zICv6uFz3Yuhke*50t+uMm7&zo6W(4MJg2}#gW{uyw8Xy?=mNd$_Xpf&m>psW zRz3P07d5@Tomxqo?%TX&yp6ALP)yu+1Wd1v^4=0t0A*_aEdqF@6jDt}XM_8LAZTe} zBZDN#({c@$r$)i(4+8$<)dwl<@zUB!hd3kv}>A7lo$JyCeeT(^uQ)pzgc z>gvVdysYTW^7Di2V z6&FMZpr!C$LFV)3ju5*o7R<=XoV0y9O$*w0kR!fB8146*n2>P9Z&s|BVV}Y64%etY z*kPPiL$bu$*Tl1Q$QV}vB2{U$F;-e;!Ih&UsioidP{!NVlhcn**P0}JztcM$w*9M@ zcYdJO2dyz*Ku&Hu)iKB3kl5g_Aye5Pu{)OZO3+uIVyjC?& zYoN7tchGR@eLfR9*ONS@!!+ejCYb4Y=s~?!-5^PGUVp#--|zcBBrk)6P(Q!iJ!d9b zO11bK;Uasu_7NL3%-A5_Ez3_S)Ty+>KK^&7MAM4E577UxxRY)Kng6Rvh1e1QH=8@j zN{ITu1n>WLRX~G)Jg|d+!2X8>{{KjEKrlWCh_j2Qjj8j0*xm}#pkQd=|KW-M-)@Zm z7HU92KytzUGxI-E9Ni;W2V1j^PBL2@Rkm7*dBqCw*|DIMx7#p-T O$wHU{YFh9=CjSRh4@+qP delta 16072 zcmZv@V~j3L&@DQ)ZQHhOn|o}V&uEWrd-mA2ZQHi3`+g@m_vGB%`cp|~b*0m(N_E$& z3JwEK4FiT(k_81r1Nt9Pb23hVmjV4x)$1I90RGc@3sHMY1b9F|cfLSCu>V0!9PC}q z>|Gf=?QD;bd0(&k(TKBVgzw@p&2Oe&>U6+=& zO&nSb>G<$=^cbC+OM7*l`_lQ%+iro&233|W8;o3FY0GZ?3{6^km|O6ec-0uZ4sODT ze!!o`7Yha1d_TQx1zyy6>F3KY+hPB>gdYe zWD4VMbUGw62!O4$wzM`c>1LC=$UDMiw{|{@xIH)9tX9cDwUt?M^r z2;lo}L?na%G-_vM2L;A;A+>A!91NXbzyw!1bK%kOUB{+4pFJ+q)#`)|pKu(EqLfvE zWL8|)ShuQLzAb=F*f}(`RRHQ&LJqCa+1P3{vX=dT*q{u#Us(s3*3IbhKvD?9#GNF1NJWi9_4GwAsC;MmY!>=66HAa86T zRKUSgut8#~C3s|*?d8<5l{^{`pR%LN^e6+Nw>i}Zb^M7`uoHuos|F~<7cSa0w@DR& zvf-Iir9JNg3r}ZMppN|NY@^dmu}3wRV~ZI>j*_g=R3xNY<1_o9YJP?ez@_*BY5=BYh*q9Ln1IwS|HwLZ5(tI zS(n>Tt1u6MdR^z3deq{E=iWRCBg)QnPc9yCL_d1e_X;twvh?@n}hX3(27P$ESO9$q?N0u|;cAgE9dqR+1OF6h__LM{9^0GN5eq2V_+DjGC*(Q8bXe$JbcOi$bS3^eqc$f|{YI+!j->g-X2OVQ4PYtMED9 zzUV&c8_K0LpLoC6Uj2G={?k3^3%W_`6)8^Xr8f2#8e=QoH{9oG1Eific$yF=<$JV{XQU)}kQYy3#H1uo2nutEUw1qP{9UZ$`aB zD~_r%C0cBW#i}eNZ(^-OJNW5qAdrw}p%f<9ID)hQW!xhi+imeKQwJ*_v?wFcanYeB zU4!%9gxQi5P;L6>Y{R;mjL@GLPtaG`ai_XW51^M&Xq8Brg#3r9J1wfwF6F3^!`r0+ zcWZaFvKo&xRM-nz!1qq7K`-%GLmPphXOQqTZ(~XxV-fQRXAKS5&6>k<{`4kp;-8QR zdH(N~zQWgS#&5arSzH>NpA5RF+xEWfol7Y?C~-GbEr+4ME~p?GD3+)w0_~0JX}I>V z7Qkqj3j-cagNc)Qh-RYCAxamT-wiL!GGxwA=HahUtp5AfH?}3ApRaIpooykP@2gMd zgt0qkDlScOhN33F_Q?2+ocMNWb}!*JP>nyu*NNfJ!@h%zIp$C$fB_rJ`%^w69hc!K zOC{v>??J9HW7Da-3*+ZP;+U5=?D)LQJ)q|aC!}ry!jr$83rPMnC8LgywZmQq)z&_0 z5#jzu6T|u0Pb98dt8DR0E1!5V@BXLsAsnEc;Qan~(pyL6@ZWhu!xJJW9Y;U-4${?Q zaAlg{TR=B^B1ScYd>3wwaqKyY62I%B~ysHksFD$_Y%6J-|~t zk$5)*uEa|8A29rqdD6BUB=5}l`A4z!Px+@h(Mg@@)U*tk(7V8)GFI1cw;ReZhx;#a zZAsq}3)_?VH9157Eu2Tqi7g&y{RY!N& z9}#<967BD~+C&^SNqhD#Isq}a8Pn#zprec>Z-3LJ!T>m^5s8rk zN~+N8?6oE74frg?602V0J0~A9jxa-3`;&D@j`1e$QNvdxgxp&kl{su&Qy3&|W9(7W z8VEgNnk8qPOi>3s@=p<&58WeIE-L+L6L~4G#3H3hb3D^l;a*xaQ!2;UzI6HSm~@FS z<>j%Q2hvrl;!7p}I+oSqG65H=s+@*-QBzf#*fi`(0$+ci(KEfJD%x~B$5$`~+MLhp zI(gNVS3m3qdIai!9Qn68>dOLnGbn-r1Jp48os-o24XNMd*S58+>D_1mEOA88b zKkuLSFV!y}@3N6)8Iz3}J5EOS)8n4k71omOp%{s7Lu<>R-;VcUpZ z(!QDZ+$=qctc9ec{+ZfNrW!xI>DUInS=bDN7s%W5pqgf!y4gh6gExC{Ytmo;&fjSre6>6gKpju(Fl_ctmNQ~#kC>@CEDr|j~%J0bNA%eyMskV$1vk$@5waC>fN$a z8r<>FuPt}hkij!kP5`1%twOj{qF;*jkoA@jIO-z(_*L?xuD&41LaYwt10ZPh0Rp^4 zyW_wkRM(m3BVol}p7jMq`EjFH^+N=c3w&6vzBmNOiM0)kF5~9G`OC)x;NYdtef8r# zsKk+3^}c^&s?y*);6QCKs*aa+os>{8$?gNmW@L<2P(%~4k^w_d|31m@TbedRi`C=E zM(S^R$QWb}fHZu_ogga6os!Kpd4U3?4#LT5WGZvkP7qQ-KJ2CS%#jkg7$Rg7-=HQt znfwR?!%CKs5>fj);}(z8hNz{xV9Rcmmzp-~$OKY{psUXE>4yc1ZJ^F!Og5sfDUpaG z)J>AgY(k| zQpDfvY*dFhzTe%Eq;iN*V{m#8P)d*wt!ASTs%tMo?2j(tQgQ(f+6f; zrEUCmAw`b5re%EcP2jcx{N4~u>I+*R%47@CU{hWe5r617+v=9vQ2*nbm+oP|xwOB$ z*<$KjRZ^1<9&x6k0z?-G@uFDWy8OhcV-Sa9^L@d#RDltr@l>D%o8e>P+d z$TjobePBY6qwM0m$5w2OXTf`ekRU}-4IO++6#A;`J5&P%9JhX z*Tda0g$w8{+Hr*r8%p-ZB!rt{3`=-$JYwE*rw~9upYXtDqly$_Kfa9kP*cdOlTM^v zbI;C)(D92n_Z%f;m-8R6Uy36k&Mn#qJOI?>b)|E@A;Q~#7P5()eG&&+8HpSPl{aWh z;t9fdo34fAfW1rI4y&qHR`)k&mN`1#=cXYHNE`0^ZGE|Ld$VE`Ee-{w;%&PGapTMn z4NZ`jJH=`=7z>S4a>+g&S*9KD;W#NqyM_d}JeO$M?++^-) zIq*@3D#IW(cwEQAFXCIYOun_-CPtqv%sSl_D8B z!hIXT5f#DNYDgej)TIiFvN4THsyWVNB--OtV)g4muH>-SNJI3L%Mq9r076M&o_Pn` z%9-#36kK#UO;#K+c;PIrMxhLO9>zPwUG{@SbtWzLyd~MSAnv%~!N}1hv!WYZY8s%z zuS;yGQX=38xU0+Pp30-2UNx$b{#GdI!x*nkPW8ysjdFyzxJWmpSEi*5SiEJ{a(B&a zN2IO|-b^JT6i)4P2h{2bfCq^1xB!E{*be;JXoT!i^212NINb896Jj|NAea;pg)CM_ zvguTOg?iZsa*>3yn2Ip$HCZ=UG%I+=wD)BtKG>loO5GCSk)Po#C(4U3l_UA#5OLlr zcvZ`!8bcVkF;hmN1gz!MBX!#7_84xl``QC4wKL3AEXvQZFWkfG0O4{qC>&#(IH}Vo zhC&$HjZhjCAvY{0swjg-i;P0BrweRNOdI$U?Tk^;?|cfHN2*y|r*!J4#0oz-`a`dl zaVEHWV9QXH5#eIEU?}R$erfhL{9&3Ro=(_GiGChs1fxHn=p(e^K$DipwzZ>FnLr%5 zD(_Jd{%&#wz7p0ofKE*A>FV?h1k8DsJc@vdzkI^j-31g`6C^#V^;Re93p}w$8O^c( z$kjmegik7?Pb)s>wc4iO3)5|wkq9Kf2{k^Yi71QSU2sHuiX&3XdPJQpGwIgGRH!^p z6d{rHgSCs?A2dlrOof5Y#2FF<5bZdkl#8rYAbb8h2Ms@K`oal)#I@D6$wiukCNnW>tUsX z^8_m}w5Ts1V;|%QY*-;#nF))fm-Ry+xJ5rKcxs^V0AhseV0(Z^UZk?9Wiaz)hJa4J z(Ms49+w7rc0VS5Rax)v9(HyNb={)tUph@dc>qoVjLdlBanPGZ!)7c-Z zrT(E_!s>+r|E>cLh&@V1H2aWKpmVafv}f|q^|7R%YDw|EMk)?Aw`o7;vQMM1!f}F2 z)PD=_C6B*QGzSdWWmw|hp>E#|#j1%&jm7@Cy9Ydf;uS&W1*WMl3=6zWmGtBz-zon%={oV(s2mL=XhG*D{M= z06WHNZsp}f?Xh$k#V+yI(qPx$Ec?-!$L>4u7I1!M+Zo_{v~9= z=6M`xB$NGQ{A?s7;D+ujq6BwRjQUsS3q9&AE)yBF## zyrak+9#B!E^=s&a5vbW3@lm2CCk~%=eo-hoQw2r}0-C9woYC=|n6C(JlnJ9wtg}@@ zJx62)Gee$sijBDEcq5*2K;GdTVh3nBbC3-ewW~-e1gjS$k9iCYn<84%he5n1fKz>) z!ZS~L5IWIUnu%VXr4XS5@nz&6#vkhBb`m@OHt(yFEE@sTp>+4+kgUrasG<)Ojxn63 zM3W7bC#RZ!Xx0)AQV{s82htc5$py+P7vpJd2O>=s#AxmtT3Y!tI_g-E%gS)Mm)Qlr z3D|XoFDBU^qddV!VZlVZrK^UAfP@^+3YFbI8rP;6;)VI^hQ}F_(99@p1r-7oE#a}O z4y|JZ(8H$};Y+4{In_JWS0v7ZYwOsQhvDsIJlrL5>+2O=B<8QuC!bFW%pI4W>C>U} zUVI!=bOj(5WZN|Jhb`MQ1QcP7c^tv~w2YT!9S(uHZLP3?XID;o;$Z1E$ z$f|0-fF22SO&z$^5C)b7Kq1@8r)&LNae6Dm6IyerV)mbs4dUT#1$_+c9PfY5%C+Lr zEmk>oC+)fA>CJlyp&qP0%aH60kF4oFMsT$CV9zZT2$+HDgRx{T_W6cJ1H8=9rvLWX zDpvf3AJIKqTz~HNn963L^ zcS#;il}0;Vok)8MLy4E>-`lmckobyIv5b?+;$X{QWihm#5>cj^-|zDi5u~%=VN%4< zVIhyis2Ev})=htQx=_S2sNOa9J%JHlm^9t|VK4qklP-I+u&o~{M*KBU0$-B9HxekK zqhgKhiGf%tSdZj61YDm`M8R>IIWj`#qQCIuIGN{hAF7FOA0rBq^kV8^e1K|&36k1{ zqT+cW@_US{pY!Z=EM zAlhUbT2CJ+Db~|c{gl@Q9pPARaH~@f8$4uQK1>Z;uX0km0gnx%pDb*h^@&?wCp#n6 zxiO#Hj5lOsgjXjfX+Ug0dcI)r9Nyiy_`><7q%bi)X17dTc5a+tr^b0z6}M_+^PZD0m;0S4aYUq6Y*+`d;H*JdMvYL*w1iGZ z^HkzMx`!X%+Rx!VPMk%)o(=yz$T8&Mm3Q;@~$r%u@B@QPojmG z=AIY=sw-d?Lj`x6WR_HKAGCU>RQr%me}Y>hl8_#b!kpv>%d=r)Q_?3j?7z1&kEK>L zP_aj+^PjkdQsUg%Z(g1vrSQqp0W#ogSKc2i+z5R0fA}E3dGf*NcQZShbJBmr968xw zv-B$&0X{;e)a$UxF_Qwnz@N1HV$10L+gEkmnL?{CJR$M`v??Uv@AKA6`W9FX*3Wy^ zByBFg8s#!>I~LAdej^^Y7qMVzF0Ys5^N^B%J^NeZtk0*jD;h`Nh1tt?R_mu>y)!qr zJ|R=a=G2Xe$gGta4lIf+HW{TEY82nyx1W2j0R3GLiw{Z$7~ ze5&p&Xp7KD1io9$d9I8wg?GRdlB~q5m5c^nK+sF{t5i7(ON1O7>LeeSf2`>UOM=|f z=e;*oKo~cx&k$ed)71-NtG}GN!;Cp5|I@AceNnhWRIbILpjeG1iSKXN{A=@8V?E&w z08lZB@PqWVCsqD!#Dq#F=*fzIv`+s}^;YoRDlWS}dqCX@BlgYC*}_z7ODtcZ>pxKd z_uZnxxSPLh#P={SzPdK^fwOt3O?t&d({Cu9ALsOvKPnxYzNYT)FSkx?t{YP5EF!bc z_`|>2JVvag!Y$W&4tJKCC_aYmu~46 z#MPP!)sW_33VoV$@Uyo7O|ZR#rq!JcrO3C9PW^izDO0_b3)X_nJtrwxkO<1zSWvVq zwhmda5poqZ-b204N%~1?3XpqI9QW_N5;QjWi{ea09Y!!&AkN)H7M6NE7m0@gIG(#m zSa`m4+{4go-S2Q|f_r!x%gy4o!RNJ6bk9YY%tcsVB=@n$XIA!WDD|+YcColVpIKC1 zU!?ZgyFPrG5B<~IE9bs4;;}+-RV5#vxsep#+1==FVNsoT#izd`?R7^$)Zl<%*m-IS zSe>6idK%Pc^l!r*5To>em{`yUy!qV_{p)1@2HGEdDl*i00D_52^_9_q^sn9mN5iM5 z?@*jbLHl$jVBr`0&EPf_V|RICqh3)>_Vew57 z(LeKz`a|{Ju*uVAKHyhl1;BgRoAmr@@$J$09aJ7IRdI~FL(eai~I@1RFGlM z0Z*OCX|)4MFc+08BXoa&rehPb0mH$x_*c7>1p`ZZ)Ygo6hfBh~Ua=tN`Gk0^`#c3E z^btvcUNI*o_O$pwyObdVOKKF{5ecVD!sjtr^#?seR(C(kQ4QmY@3D{%^CT=p^r9m} zq5zEjMdXWClEx<>FMd)Du4%4+wc;QwYLp@5+OhkrEk;BucxN+To$l%=a8 zpf!6XOgyNiW=!;}a(g<8$?NscFm#sJcB_a_hLb={cM>UTX2(~oLXOqDNU-c|@qd5Y>1o;7>1i+MQPk^tD9Z}rLgBzv z1U?}pihZNW($j^G(m-<>&_sp-FdIU9h@0S{i_xNYCss8iAi>iG7 zqx2svuVF5kYq+9m%Z!Y}Jg0#6sT>hEsq{|@kIVqP`G}4v2tGYBNj*SUmzVe;Sx5f5jFv&*prsTL0p=!eH?o{QnW2f;3KkIJ0Bu#J3!;+ zt&dbeiwOO&9Rb178tq1IbWx=!4md)x{OP>As7@wf>WE%b4^k@K98!% zc1rTkU1>8qX=q>cJ%fHhvD9K6J}VO*Di{KBo`?Y=)zASa>N~)sL{#9fNrl!Kfw-@0 z9QHk;OZFG=ykJfY<#L{uR&4UD6_OIN9y*u$<;Dj=c9IhH-D2rFnV51A2yK`r7+hrT zmC0Ed4Bt-I;>0uLnw&NIp6C2jl~6Rp6Q3G*S`j;aSh$_EDdZZYV6F#EP3yo`2rLc* z9Giydi@Nr^OvDJ zFQvLz?*-Dgwa`4~>a6R-RJ3C5dO1REnJv%x+&VB*z(Ng#l#Ex?3{RnYkpy-%eI)L~ z{qe(J0z;7@aC5yYCQ1r)1Ja+VV9slElTgyt;w}I`MBZV)J_292#o~2#RY0|2dkr}R9}v(d-rYPkLQ3wL%2Q0X0&@= zc`ehWkSgL&51d3KR3YKfdmlec!t z*eXCE+j?(-jx{%A^lav2EL0aB69^_K9*3latdFO}iAta!`isfW2ppF`naJuLjZzWJ z$yB6VsWx^N715@pU3(u|qb*|StiHV07dH*J=wr4?cGn`z)J>iUQhPHYZ-p_8L3vB` zdj8n9^2*G=$nrX?#zY<&?(rJJJ2Hl<&0fIbJh21trzZ54`KZdCg6IS+c^GNUz?DMM z>jc7Kpq7B&u~Wx*N18LsC$Hc0Y<(9DM1kG>MGah)adIj$AKo!Aed{Y`k;30RHPgL={hwEcTFYIgwmBwu!?eu%6(j(S zOzS20=udqn*@28A43)6{8fBb~EdF5x<7kgMin zRuz-qwZ`0%rN`oL^<9w09ACrWTfRm*yw%_J%Eq7(sM4IG*{cQ>VylRn`T1++MjWFV zWrUAAAgR~2MouNqVlogrI&rc$%ol*HNeN2@AMkJ_7TLp}XY+s_Bg(wINR&!t(OES( zOB_EY0n4-LFu3k_OI#NQ1knyaIBIf~j&>>-Uf3HU&g?6&f===o&6@qv{bi`iur%-^mqE6xg zupn!S_w~!!i)c~AGsZ-`HU;VYio$6m?+JJD8lmRQ0G3_7T1|VVqf@y;ZHZ{sh?+Ev zcxmj8I@p;8GPa61Zi&fCaXHD5KYjYi{tqz{DHSr?_eWpzWH~_e&>1!%D3^$iMpDD~OjlNFV4st(9a6aZRy(m$*udAf z?ts}H8ipg;D?Lyd9IBy(1gGDhEx#(j`fpv;{d#0zWQ~we&lp4jL)0)8K3@6i(c?sQ zynR9EkFKAx+j8uL*;SA_olW|Q{NCE=^<$?eKLH*ZeWM}4T^glgDF^R(e zRe90GUbz8(kNF|8i)G@k>1dw8#(|N(+Nj=8jZjK0%Dww@3pwhyDJo^?;8-0_aFws@ zW8`c0bPwy~yhawll(_Bc7#0VAVr&=mo2j<$EzzK^mr8))iUWy~2!=JbN?5u*(G*(# zp;O^niYpD|08@&6xkYKY3St-j$Rh6W@}7KU@Z|wOnrRkak!FGx9)~xl-ZQPm`v>Be zRC1koyc|~ctXe$u)7&PZ?ylWsi!wE$Oh0}LdUq+r@roh97=s^|E1Oo93>JKI>g7jZ zK1{K&s>PW4MbzisU1nG;gA!7y5Rtj|p!LH{<{EiS@_m493I&q?wj8{BljVlg8%_eL z&pha24y1-0`}Ck>N1#WciaRO9s(&vhCK%x^)OK04X*u<4n9LVBTxFbWb>7y_RyOfq zd?^^tg~8^uBHmW&d;$I6_f1u!z}Sfi1eAvb1cU{Ygd~asU{CkpTfW5|K}<=Jf0N;I zPsDF{rI2~aHB0($p~dtTy#X2;d)*2GJRS4{#RDjF;1fyCCA}u=F9jZP~tXn{*@nMiK3s)VWyn ze*rHLE;#HiDOp-xY+7z@Q+_*m%V{p#>F~gt`mfqRFm|TU_sO83XJST90|Gj|n;U05 z$o;M=?!{f`kHh)CcyL507W~gxRUAC5u%Y4pex<3Z&{!zS=uxf+5>gOwHvE?o9~5+Y zFf9F-T$na+(Fe!u`}w{cZA1%uWmn5*jT*@_TG<-!8$(^ zgI&f@aK}WR9taM-5_l`qrXe049-h}sKm-fA3sF27N$0O{pJ6myUu*jPkslc!Zteuw zJIdSd`N`B)SL5Ro#)VQ*G1z~;#=Q^d7MHeaJ`YFf{g-Hv#X88!$fz%UlJv;JKtq48WReD`9Ci|yWYXm2-cJ20QR95-o88M*-<&EogRq<1 z;$^1}Z?_wg^Q=%Wa2qeZ6*4gJ;p5|1>pDGnnIaN2{eDkAoiCO;cWN88O8AMBlS3L+ zj)dB)3VyRhmCw)5SKU9;J(D2V-vs0mc&&Gxu6cUOZ^gh98q0(l!he6DA2|WK*mv(4JDD(KLgpnPx-`^*{mms zjODDA`~N{%eAR5;7R^vqWLME8d#~DAd3gAH zdA;Xcd#t`<=aoQ4*ZT0Q<$eIjrb$aW=9@1msAwrUf0g?_gW{;wwzjeZGcve+oMjah z5CX>tuQeo$JzU)@tS#eL%1AQ{>e^=sb9-e~;2s`D-JG2#s;ZCGvz3&Tq~v5C@Eh4| z^vXu$v+EEpIA7Cdm}z_-j>fyjo~a2ccUpr_32|Xf2u9r-A0HqjB@O^5CnqxnQQ6_& zzPg60q9S5qVt+VTcq-g%g(SIlUvi<^we|Jo>-1HW=J+yyuOz7vq*qyHh$)s>b) z2G-Tr3)|-sIzW14Mf!L_Yjd+Ao0PzK4QaAleW6|w)wSuJMHJI-uT5>lKl5c)VdMIY zO9oz2NUd{nX|d;mTKE81m2YB_lat&WPaYNH_EnTD9p`0@&|@gV$O#& zTnAKZ+pIjjq<6c#28Tv4_`YcNyo~zxF5*@+)I>-M>CpelJs0j55a*?0)HA3mpda*& zOJ8qwbz#^LKvNjwu-KZkCHN8$^Yeet(b}hCePC&$>u)G<#cXuHQ&TeiLj>R4&%PT8Mxms%%<@tF8b;D8$KW6YN+8y&eVML=6jf%FL zKzs-574GtoT2&O((UH-zBU0Q#_ofqqDxxZBZ%+$JOhsx3U!vz6YzWSl7?+pPlVNWZ z7#P;)@aD?&_4NV0&~=3!-|~O&sfu{E*}6@t78V-3+zjPXxCDrS z8I~J57@Z_g`^z-_)zjJSMOS1vCUi8&fco@8n zA0MBm1%j{wfEOE1d*F5(){}tBjamdgA_`(F{p~zMOGJQ=naw7$+;I$Wr`$PW0}i9z zy1>l)6Lj^tZ55sB-}0}?v`zHP{ZUo|PpF=bn0%t9;6sxnTz(A|7G8Fn%o5`Og;mTa zzWy+gPJKfH**m{3f2t_I2BeV{^j9fT(}B!DGCHN!ab2p{g`4enKT{#LvW~-#+R@lg z+VAfcWt83EMzY$Bv-x zb1`_{B7F>Kc90oU2ELQ9UijC3c9+wx>M4zM0a&lCnY9wAXL&p1iSkfkvba8!bUiJ z{OQ01#BjySoypSXNPIh1_!?*NAX4(wyN7aR-Lx(~G!2jd+Wn)EPh37zH$NOJzrW>DI(e|m_Vem}~d=ib_PveDAgE)8jE zTTgMy)Ybi&66NdhZ)5r;UHjljfIy{1mc9r>^jFj5?Jwt8GksuhZZ2NnW{ycltRLT^ z60EE&dkc_NS>$e~CR*P1s9~6urw7oBR(_mb7wzg_Ta?+?n#*Q(DM-tATS;Jjci;c3 zq-_7TQApvqRq$8(+k{K%_H*cohc8Z!0_NCI-ybAPy|zs3U5Pz2UZ;~KDnwLOt9bwV z5B6`khc&xl{YR0b)kfpvV|sD%tE42uJ)C(GCrxcNzK+%#Q_5^VqGWa&9|+)SVuMvb z37k_lN&Xi1o`7}?i-O`LIm`jnHMJ{fz&^bnT>gy{hJ{7hokbe_Z|9n2YhqH;u>J`P z*zN~7#7;Zc7^YP%(>dC!D8=#+i-7K${*|k)ybJp7#q9-3$;hiRV8Eyc>BtO90esYzFa;u+C$U9tkm1P?qUlu!)Of>0)k-GSzXx~P9a^B zTzIs*ockGVhgW|(9KFQezvDwbO`Th5WFjOdNmcL7H6mP9NCnS`0l zOio6%n8W`BE|d|nGczM!14|}WOhMIlGrOC-z(y>vKX@JFE6El3*Et};9;*o?*SYr| zo3UbQBBncF?G><7=0p4Zuu))ONFz}|NQ^{I$OtOEC&@bGCIbXS-L1Vpc>k#F%?q_h zSXrrDnF2SsR=cl`YyWrFz%kXv?{$P3r{nu;wYvSw6k*>gy@@`GklUK_K!-+UM&~!;R zvu)VHgGNh)Nh3F8+i^*uN;3@nmc6x=s7aYavfwnBx-8o)pT=Pd(tK`nnU!V!(`$J< zwKFQ=vIWSlVkkS!8URLqdU&|FxJ?7ew>O~U#4zdJNaD9jf@J+O~qPm_|z1a zF4Q_pi5PEJ+t(VlvVg`(%fEO_;(dU!#RlIszGHZAuPpR@YOEJPJk~c3KvuEoKa&F4 z9ON{1kDI?2`Of@?*qL5i{CS-J2w_7p@bUTHw4LjW4FF-GXI~x}DrdY2_Q=DrB0B<@ zNC8Z2tY6`2nbF=xid91 zG$gi;5GZ*i0vypm820PmGrhg942$7-p$bEMX|mkR zE4h|sRt1S@JuE!$8f^p*4pwz_@j5vGbF`p)lgrNp%VEE`CF^VlvfJ-xio zpCD?%BcTjsg733Fa%EZDnMTu1Lt5FBZ%`^8oS|~)fpKwj!?%z3N0meG;;twVOAnB7 zv{c5)R?zRqrKF@3RtyQz6B?UPN{pfBJ*1WG`Z2C|wA$(=U?uPxYx^yZe3-F(d;pT= z3X(!PZ~KS?>{&AE`Vj4GZdr$nLH>Xk4CISigV&2gE#ivZ#PID zL(3_gT!`RL=|h*Rh|9~%*R{1>RR2{yw`WqUK4VeL&MDeQd&t}CjE4m~jy2UFs5Ges z2dm7ptoiKT#{-PriJ$xRyMA6)&wl8y+%oN34zQw`mlwb7A_y6M;A;e)Ov1|i?wL3w zx}9Vr$c?Lbc|OP8@c*?9U*DV)IBFtU3@&ejsZ-2S|NDr930e+p(iI?9c?QH0@F!}4 z+^3YFLI%_ctyhPJ<8bxtAS6%7$D-n3U?k-dK%eC1`#-8nJ&fe$qD{QlliwUOcl4oz zgoGUDM>E;;5eRq`l%*~oIkuoUJEuE}5p@ao_oG@!0lOG3&KZYOSu+~8ELs80f4>*O zx|LTQ;R)&2CDMj&{P#u;Ujz)9a)n!*2ig5&zRB z8du#i62{olaIBDHPlkr2SGV5az5mBJR zEak>eF81v8ydTJo1EE4~>qRd%_bAVTx&j&kvnEPqrPJZaU0izuA_@Q3)5EqFMi#@! zH8ubeLwyQhI%!o_&Ih#3MLv$6R?yH)J4k-~iy+<82Lcgz8t z7EbG2{LlyvY;RMd(Kcp(cb>hTDln8GBTpL*(P=IW#Yx@9)5c%t<}8+BXn<`anj@bm zz2`o>c)Q&h`)sD0%n;PKtW41s4}VghpbQ-gKk`Q8lM$cC`T_6}-E;u)oY}&cOT}bz zas{NE!mYu*U{xK4_nIe>y9GtVU##f&Jw%{|qn=d1CiZr0dFKZmvED~|NBE*A=f zYAKQesB7gUXTxFyZv1@Gkr{FY$Z}bimp`+xPwl5LR%w%{r za=_&b?@EL&1mMr%0gj=ee$ZkCT1aond`n?FGER!^q2Y49leD|fU6 z{dFmggU|5MfUK;*iFc1Y4fW)dec;O^Wu>MP=(dU`eLz<+)hYvVQDLI8I(fi2QJ=&i zeCulm*FI1--$ph}Puty77ayJ^f`GYM3|Ssajl}mupS+E6kmw%t5S-_RpP-qRvo8!a z5H@9=i9p9G1(uL{O(G| zwL)PxVu}zzC)?lM+opk#>(~f94=>I7$s>f>9N{7i98hQ9zY({FLm3q4lo9hDnJ&C3 z0$3`B@>xzh;0#f?{~3*EzW?L6O@559G-m3`+Ib!^QmAa6q@;F_PshSe>vnPtiP^?O zE903}+08_S7l2?7i0|$FmA@h(BLP8z4a@E`nTZoC6f(|iEyX};yWNq3u54pQJ@$pR zvvYQXhi8T}WH+aFb3>v0fpiVwRDqP|18}o9cd{}9H?-6n(Vw|Vd_A43<}94-bm}y{ zE^;VZp47dh)e}gfL~(BUkipHl2I-tC(&&B!zV(R4Ew>;`%QjPvW`(C)$tr1ej3BoYxnKNcaDyX#A{avWZhY% z(_I9#_mI^zjMyeU(eTk2U0GlMJpJqz6}3)`3=$CqQQXf-wn){&xEL_hR@tH-#ZnmX z$Hl+YmhT6e4j;~`X-mI798ZFr0VX-^BOJ<95|h@sxuE_GQ&RdSB_&Nqb-rBo%KX_R z^OFFJ97K&z2c$cbtgjUs6bqWV=-;$U_FE`!kE-fugx0XZ2=M!~A9qO63gt@8W$|pF zzeyFj*0A5z(2<>gqHf7VIi>=Y3{~n{To&Na3ie(!7|^NcbAIlKSe%|x0w|d+$xsm6 z%!~D`?d@9@a0=FGuCoP>STzI_cf>h*BW?l1`c}f8n}%26Q?bJI$VmjrG5jTjLfgi7 z-Ke|Mb+_4u6)3~MBu07Pbjx;gZhhjR-^*@36l0ALXP%DBN8J`h0&OZ_LKG-C;7wn* zFYc%JuOoIpS@|Is!7TZ#j{k_tqy)&Kt}o&STeq5MAtkCH4f2pZ`BjGsgO q&-l6C|4mH%?>#_3#er){h@zDL>tNiXxWMB{>Y`K-d&2*N_ half form --- # After ccmp, consonants are in PUA form, so reference PUA here. @@ -915,7 +924,9 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None): f" sub {glyph_name(SC.MARWARI_LIG_DD_Y)} {glyph_name(SC.DEVANAGARI_VIRAMA)} by {glyph_name(SC.MARWARI_HALFLIG_DD_Y)};" ) if half_subs: - features.append("feature half {\n script dev2;\n" + '\n'.join(half_subs) + "\n} half;") + half_body = '\n'.join(half_subs) + features.append("feature half {\n script dev2;\n" + half_body + + "\n script deva;\n" + half_body + "\n} half;") # --- blwf: virama + RA -> below-base RA (rakaar) --- # This serves two purposes: @@ -938,7 +949,9 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None): f" sub {glyph_name(SC.DEVANAGARI_VIRAMA)} {glyph_name(ra_int)} by {glyph_name(ra_sub)};" ) if blwf_subs: - features.append("feature blwf {\n script dev2;\n" + '\n'.join(blwf_subs) + "\n} blwf;") + blwf_body = '\n'.join(blwf_subs) + features.append("feature blwf {\n script dev2;\n" + blwf_body + + "\n script deva;\n" + blwf_body + "\n} blwf;") # --- cjct: consonant (PUA) + below-base RA -> RA-appended form --- # After blwf converts virama+RA to rakaar mark, cjct combines it @@ -948,7 +961,10 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None): # # A second lookup converts RA-appended + virama -> RA-appended half, # since the half feature has already run before cjct. - cjct_lines = [] + # Lookups defined OUTSIDE the feature block so they can be referenced + # from both dev2 and deva script sections without name collisions. + cjct_lookups = [] + cjct_lookup_refs = [] # Lookup 1: consonant + rakaar -> RA-appended form ra_append_subs = [] @@ -956,17 +972,18 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None): ra_form = internal + 480 if has(internal) and has(ra_sub) and has(ra_form): ra_append_subs.append( - f" sub {glyph_name(internal)} {glyph_name(ra_sub)} by {glyph_name(ra_form)};" + f" sub {glyph_name(internal)} {glyph_name(ra_sub)} by {glyph_name(ra_form)};" ) # Marwari DD + rakaar -> DD.R (DD stays as uni0978, not PUA) if has(SC.MARWARI_DD) and has(ra_sub) and has(SC.MARWARI_LIG_DD_R): ra_append_subs.append( - f" sub {glyph_name(SC.MARWARI_DD)} {glyph_name(ra_sub)} by {glyph_name(SC.MARWARI_LIG_DD_R)};" + f" sub {glyph_name(SC.MARWARI_DD)} {glyph_name(ra_sub)} by {glyph_name(SC.MARWARI_LIG_DD_R)};" ) if ra_append_subs: - cjct_lines.append(" lookup CjctRaAppend {") - cjct_lines.extend(ra_append_subs) - cjct_lines.append(" } CjctRaAppend;") + cjct_lookups.append("lookup CjctRaAppend {") + cjct_lookups.extend(ra_append_subs) + cjct_lookups.append("} CjctRaAppend;") + cjct_lookup_refs.append(" lookup CjctRaAppend;") # Lookup 2: RA-appended + virama -> RA-appended half form ra_half_subs = [] @@ -974,15 +991,21 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None): ra_half = ra_form + 240 # +240 from RA-appended = +720 from base if has(ra_form) and has(SC.DEVANAGARI_VIRAMA) and has(ra_half): ra_half_subs.append( - f" sub {glyph_name(ra_form)} {glyph_name(SC.DEVANAGARI_VIRAMA)} by {glyph_name(ra_half)};" + f" sub {glyph_name(ra_form)} {glyph_name(SC.DEVANAGARI_VIRAMA)} by {glyph_name(ra_half)};" ) if ra_half_subs: - cjct_lines.append(" lookup CjctRaHalf {") - cjct_lines.extend(ra_half_subs) - cjct_lines.append(" } CjctRaHalf;") + cjct_lookups.append("lookup CjctRaHalf {") + cjct_lookups.extend(ra_half_subs) + cjct_lookups.append("} CjctRaHalf;") + cjct_lookup_refs.append(" lookup CjctRaHalf;") - if cjct_lines: - features.append("feature cjct {\n script dev2;\n" + '\n'.join(cjct_lines) + "\n} cjct;") + if cjct_lookup_refs: + cjct_feat = cjct_lookups + ["", "feature cjct {"] + for _st in ['dev2', 'deva']: + cjct_feat.append(f" script {_st};") + cjct_feat.extend(cjct_lookup_refs) + cjct_feat.append("} cjct;") + features.append('\n'.join(cjct_feat)) # --- blws: RA/RRA/HA (PUA) + U/UU -> special syllables --- blws_subs = [] @@ -1000,7 +1023,9 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None): f" sub {glyph_name(c1)} {glyph_name(c2)} by {glyph_name(result)}; # {name}" ) if blws_subs: - features.append("feature blws {\n script dev2;\n" + '\n'.join(blws_subs) + "\n} blws;") + blws_body = '\n'.join(blws_subs) + features.append("feature blws {\n script dev2;\n" + blws_body + + "\n script deva;\n" + blws_body + "\n} blws;") # --- rphf: RA + virama -> reph --- # Must include BOTH Unicode and PUA rules: @@ -1009,16 +1034,20 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None): # to its PUA form # - PUA rule: matches the actual glyph after ccmp/locl has run if has(ra_int) and has(SC.DEVANAGARI_VIRAMA) and has(SC.DEVANAGARI_RA_SUPER): - rphf_lines = ["feature rphf {", " script dev2;"] + rphf_rules = [] if has(0x0930): - rphf_lines.append( + rphf_rules.append( f" sub {glyph_name(0x0930)} {glyph_name(SC.DEVANAGARI_VIRAMA)}" f" by {glyph_name(SC.DEVANAGARI_RA_SUPER)};" ) - rphf_lines.append( + rphf_rules.append( f" sub {glyph_name(ra_int)} {glyph_name(SC.DEVANAGARI_VIRAMA)}" f" by {glyph_name(SC.DEVANAGARI_RA_SUPER)};" ) + rphf_lines = ["feature rphf {"] + for _st in ['dev2', 'deva']: + rphf_lines.append(f" script {_st};") + rphf_lines.extend(rphf_rules) rphf_lines.append("} rphf;") features.append('\n'.join(rphf_lines)) @@ -1035,8 +1064,9 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None): pres_lines.append(f"}} AltHalfSha;") pres_lines.append("") pres_lines.append("feature pres {") - pres_lines.append(" script dev2;") - pres_lines.append(f" sub {glyph_name(half_sha)}' lookup AltHalfSha {glyph_name(la_int)};") + for _st in ['dev2', 'deva']: + pres_lines.append(f" script {_st};") + pres_lines.append(f" sub {glyph_name(half_sha)}' lookup AltHalfSha {glyph_name(la_int)};") pres_lines.append("} pres;") features.append('\n'.join(pres_lines)) @@ -1116,10 +1146,11 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None): if abvs_lookups: abvs_lines.append("") abvs_lines.append("feature abvs {") - abvs_lines.append(" script dev2;") - if deva_any_glyphs: - abvs_lines.append(f" @devaAny = [{' '.join(deva_any_glyphs)}];") - abvs_lines.extend(abvs_body) + for _st in ['dev2', 'deva']: + abvs_lines.append(f" script {_st};") + if deva_any_glyphs: + abvs_lines.append(f" @devaAny = [{' '.join(deva_any_glyphs)}];") + abvs_lines.extend(abvs_body) abvs_lines.append("} abvs;") features.append('\n'.join(abvs_lines)) @@ -1137,8 +1168,10 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None): all_lookups = matra_lookups + ya_lookups + anus_lookups all_body = matra_body + ya_body + anus_body if all_body: - feat = ["feature psts {", " script dev2;"] - feat.extend(all_body) + feat = ["feature psts {"] + for _st in ['dev2', 'deva']: + feat.append(f" script {_st};") + feat.extend(all_body) feat.append("} psts;") features.append('\n'.join(all_lookups + [''] + feat)) @@ -1168,9 +1201,10 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None): calt_lines.append(f"}} InterwordVisarga;") calt_lines.append("") calt_lines.append("feature calt {") - calt_lines.append(" script dev2;") - calt_lines.append(f" @devaFollowing = [{' '.join(deva_following)}];") - calt_lines.append(f" sub {glyph_name(visarga)}' lookup InterwordVisarga @devaFollowing;") + for _st in ['dev2', 'deva']: + calt_lines.append(f" script {_st};") + calt_lines.append(f" @devaFollowing = [{' '.join(deva_following)}];") + calt_lines.append(f" sub {glyph_name(visarga)}' lookup InterwordVisarga @devaFollowing;") calt_lines.append("} calt;") features.append('\n'.join(calt_lines)) @@ -1571,9 +1605,12 @@ def _generate_mark(glyphs, has): # and would bloat the GPOS table). _EXCLUDE_RANGES = ( range(0x3400, 0xA000), # CJK Unified Ideographs (Ext A + main) - range(0xAC00, 0xD7FF), # Hangul Syllables + range(0xAC00, 0xD800), # Hangul Syllables range(0x2800, 0x2900), # Braille ) + # I-matra glyphs excluded from MarkToBase (they should not attract + # mark attachment — marks attach to the consonant, not the matra). + _EXCLUDE_CPS = {0x093F} | set(range(0xF0110, 0xF0120)) all_bases = {} marks = {} @@ -1583,7 +1620,7 @@ def _generate_mark(glyphs, has): if g.props.write_on_top >= 0: marks[cp] = g elif g.bitmap and g.props.width > 0: - if not any(cp in r for r in _EXCLUDE_RANGES): + if cp not in _EXCLUDE_CPS and not any(cp in r for r in _EXCLUDE_RANGES): all_bases[cp] = g if not all_bases or not marks: @@ -1758,30 +1795,39 @@ def _generate_mark(glyphs, has): lines.append("") mkmk_lookup_names.append(mkmk_name) - # Register MarkToBase lookups under DFLT (for Latin, etc.) + # Register MarkToBase lookups under mark for non-Devanagari scripts. + # For dev2/deva, abvm already includes these lookups. Registering + # mark/mkmk under dev2/deva too risks double-application on shapers + # (CoreText, DirectWrite) that may process mark AND abvm separately. + _NON_DEVA_SCRIPTS = ['DFLT', 'latn', 'cyrl', 'grek', 'hang', 'tml2', 'sund'] lines.append("feature mark {") - for ln in lookup_names: - lines.append(f" lookup {ln};") + for _st in _NON_DEVA_SCRIPTS: + lines.append(f" script {_st};") + for ln in lookup_names: + lines.append(f" lookup {ln};") lines.append("} mark;") - # Register MarkToMark lookups under mkmk + # Register MarkToMark lookups under mkmk (non-Devanagari only) if mkmk_lookup_names: lines.append("") lines.append("feature mkmk {") - for ln in mkmk_lookup_names: - lines.append(f" lookup {ln};") + for _st in _NON_DEVA_SCRIPTS: + lines.append(f" script {_st};") + for ln in mkmk_lookup_names: + lines.append(f" lookup {ln};") lines.append("} mkmk;") # For Devanagari, HarfBuzz's Indic v2 shaper uses abvm/blwm # features for mark positioning, not the generic 'mark' feature. - # Register the same lookups under abvm for dev2 script. + # Register the same lookups under abvm for both dev2 and deva scripts. lines.append("") lines.append("feature abvm {") - lines.append(" script dev2;") - for ln in lookup_names: - lines.append(f" lookup {ln};") - for ln in mkmk_lookup_names: - lines.append(f" lookup {ln};") + for _st in ['dev2', 'deva']: + lines.append(f" script {_st};") + for ln in lookup_names: + lines.append(f" lookup {ln};") + for ln in mkmk_lookup_names: + lines.append(f" lookup {ln};") lines.append("} abvm;") return '\n'.join(lines) @@ -1826,80 +1872,111 @@ def _generate_anusvara_gpos(glyphs, has): lines.append(f" pos {glyph_name(anusvara_upper)} <150 0 0 0>;") lines.append(f"}} AnusvaraUpperShift3;") - lines.append(f"lookup AnusvaraUpperShift3Down2 {{") - lines.append(f" pos {glyph_name(anusvara_upper)} <150 -100 0 0>;") - lines.append(f"}} AnusvaraUpperShift3Down2;") - # --- Lookups for regular anusvara (uni0902) --- if has_regular: lines.append(f"lookup AnusvaraRegShift2 {{") lines.append(f" pos {glyph_name(anusvara)} <100 0 0 0>;") lines.append(f"}} AnusvaraRegShift2;") - lines.append(f"lookup AnusvaraRegShift3Down2 {{") - lines.append(f" pos {glyph_name(anusvara)} <150 -100 0 0>;") - lines.append(f"}} AnusvaraRegShift3Down2;") + # --- MarkToMark: anusvara attaches to complex reph --- + # Without explicit MarkToMark, two marks on the same base get + # shaper-specific heuristic stacking (HarfBuzz, DirectWrite, and + # CoreText all disagree by ~100 units). MarkToMark gives the font + # explicit control and suppresses those heuristics. + has_mkmk = False + if has(complex_reph): + mkmk_lines = [] + if has_upper: + mkmk_lines.append( + f" markClass {glyph_name(anusvara_upper)}" + f" @anuUpperToReph;") + if has_regular: + mkmk_lines.append( + f" markClass {glyph_name(anusvara)}" + f" @anuRegToReph;") + if has_upper: + mkmk_lines.append( + f" pos mark {glyph_name(complex_reph)}" + f" mark @anuUpperToReph;") + if has_regular: + mkmk_lines.append( + f" pos mark {glyph_name(complex_reph)}" + f" mark @anuRegToReph;") + if mkmk_lines: + lines.append("") + lines.append("lookup AnusvaraToComplexReph {") + lines.extend(mkmk_lines) + lines.append("} AnusvaraToComplexReph;") + has_mkmk = True - lines.append("") - lines.append("feature abvm {") - lines.append(" script dev2;") + # Collect contextual positioning rules into NAMED lookups so that + # both dev2 and deva script sections reference the SAME lookup index. + # Without this, feaLib creates separate anonymous lookups for each + # script section, and shapers that merge both dev2/deva features + # (CoreText, DirectWrite) would apply the shift TWICE. + # + # NOTE: complex_reph + anusvara cases are handled by MarkToMark + # above (AnusvaraToComplexReph), NOT by ChainContextPos. + abvm_rules = [] # --- Rules for anusvara upper (uF016C) --- # After reordering: base, [matras], reph?, anusvara. # When reph is present between matra and anusvara, use 3-glyph backtrack. # Rules ordered longest-context-first (first match wins). if has_upper: - # Complex reph → always shift3down2 (directly before anusvara) - if has(complex_reph): - lines.append( - f" pos {glyph_name(complex_reph)}" - f" {glyph_name(anusvara_upper)}' lookup AnusvaraUpperShift3Down2;" - ) - # Matra + simple reph + anusvara (3-glyph context: matra in backtrack) if has(simple_reph): if has(0x094F): - lines.append( + abvm_rules.append( f" pos {glyph_name(0x094F)} {glyph_name(simple_reph)}" f" {glyph_name(anusvara_upper)}' lookup AnusvaraUpperShift3;" ) for cp in [0x093A, 0x0948, 0x094C]: if has(cp): - lines.append( + abvm_rules.append( f" pos {glyph_name(cp)} {glyph_name(simple_reph)}" f" {glyph_name(anusvara_upper)}' lookup AnusvaraUpperShift2;" ) # Matra directly before anusvara (no reph) if has(0x094F): - lines.append( + abvm_rules.append( f" pos {glyph_name(0x094F)}" f" {glyph_name(anusvara_upper)}' lookup AnusvaraUpperShift3;" ) for cp in [0x093A, 0x0948, 0x094C]: if has(cp): - lines.append( + abvm_rules.append( f" pos {glyph_name(cp)}" f" {glyph_name(anusvara_upper)}' lookup AnusvaraUpperShift2;" ) # --- Rules for regular anusvara (uni0902) --- # Regular anusvara has no matra trigger (else it would be upper). - # Only reph can trigger a shift here. + # Complex reph case handled by MarkToMark; only simple reph here. if has_regular: - # Complex reph → +3px X, -2px Y - if has(complex_reph): - lines.append( - f" pos {glyph_name(complex_reph)}" - f" {glyph_name(anusvara)}' lookup AnusvaraRegShift3Down2;" - ) # Simple reph → +2px X if has(simple_reph): - lines.append( + abvm_rules.append( f" pos {glyph_name(simple_reph)}" f" {glyph_name(anusvara)}' lookup AnusvaraRegShift2;" ) + # --- Emit named lookup --- + if abvm_rules: + lines.append("") + lines.append("lookup AnusvaraCtxShift {") + lines.extend(abvm_rules) + lines.append("} AnusvaraCtxShift;") + + lines.append("") + lines.append("feature abvm {") + for _st in ['dev2', 'deva']: + lines.append(f" script {_st};") + if has_mkmk: + lines.append(" lookup AnusvaraToComplexReph;") + if abvm_rules: + lines.append(" lookup AnusvaraCtxShift;") lines.append("} abvm;") return '\n'.join(lines)