From 41b7bdff0fa43ef5004d7a70313feb2364c3723c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ju=CC=88rg=20Lehni?= Date: Wed, 20 May 2026 10:46:25 +0200 Subject: [PATCH] Add support for GSUB Type 8 (Reverse Chaining Contextual Single Substitution) - Fix the GSUB Type 8 struct to include the missing `backtrackGlyphCount` field; without it the binary reader couldn't decode the table at all - Implement Type 8 application in GSUBProcessor: check coverage at the current glyph, verify backtrack + lookahead context, replace the glyph with `substitutes[coverageIndex]` on match - Iterate Type 8 lookups right-to-left in OTProcessor's applyLookups (per spec) so lookahead always sees post-substitution glyphs and backtrack sees the pre-substitution ones - Add Noto Coptic as a test fixture (its ccmp feature uses Type 8; without this fix any Coptic layout throws "GSUB lookupType 8 is not supported") --- src/opentype/GSUBProcessor.js | 19 ++++ src/opentype/OTProcessor.js | 29 +++--- src/tables/GSUB.js | 3 +- .../NotoCoptic/NotoSansCoptic-Regular.ttf | Bin 0 -> 28084 bytes test/data/NotoCoptic/OFL.txt | 93 ++++++++++++++++++ test/shaping.js | 13 +++ 6 files changed, 143 insertions(+), 14 deletions(-) create mode 100644 test/data/NotoCoptic/NotoSansCoptic-Regular.ttf create mode 100644 test/data/NotoCoptic/OFL.txt diff --git a/src/opentype/GSUBProcessor.js b/src/opentype/GSUBProcessor.js index f5672d5d..43a3e859 100644 --- a/src/opentype/GSUBProcessor.js +++ b/src/opentype/GSUBProcessor.js @@ -185,6 +185,25 @@ export default class GSUBProcessor extends OTProcessor { case 7: // Extension Substitution return this.applyLookup(table.lookupType, table.extension); + case 8: { // Reverse Chaining Contextual Single Substitution + let index = this.coverageIndex(table.coverage); + if (index === -1) { + return false; + } + + // Backtrack/lookahead contexts are checked against the current + // glyph in the buffer. The iteration order in applyLookups is + // reversed for Type 8 so lookahead always sees post-substitution + // glyphs (per OpenType spec) and backtrack sees pre-substitution. + if (this.coverageSequenceMatches(-table.backtrackGlyphCount, table.backtrackCoverage) + && this.coverageSequenceMatches(1, table.lookaheadCoverage)) { + this.glyphIterator.cur.id = table.substitute.get(index); + return true; + } + + return false; + } + default: throw new Error(`GSUB lookupType ${lookupType} is not supported`); } diff --git a/src/opentype/OTProcessor.js b/src/opentype/OTProcessor.js index 80d1ce69..53fa9bec 100644 --- a/src/opentype/OTProcessor.js +++ b/src/opentype/OTProcessor.js @@ -191,22 +191,25 @@ export default class OTProcessor { for (let { feature, lookup } of lookups) { this.currentFeature = feature; - this.glyphIterator.reset(lookup.flags); - while (this.glyphIterator.index < glyphs.length) { - if (!(feature in this.glyphIterator.cur.features)) { - this.glyphIterator.next(); - continue; - } - - for (let table of lookup.subTables) { - let res = this.applyLookup(lookup.lookupType, table); - if (res) { - break; + // GSUB Type 8 (Reverse Chaining Contextual Single Substitution) is + // applied in reverse direction per OpenType spec — process the run + // right-to-left so substitutions never feed back into the lookahead + // context of an earlier-in-text glyph. + let direction = lookup.lookupType === 8 ? -1 : 1; + let startIndex = direction === -1 ? glyphs.length - 1 : 0; + + this.glyphIterator.reset(lookup.flags, startIndex); + + while (this.glyphIterator.index >= 0 && this.glyphIterator.index < glyphs.length) { + if (feature in this.glyphIterator.cur.features) { + for (let table of lookup.subTables) { + if (this.applyLookup(lookup.lookupType, table)) { + break; + } } } - - this.glyphIterator.next(); + this.glyphIterator.move(direction); } } } diff --git a/src/tables/GSUB.js b/src/tables/GSUB.js index 757bf5a1..c14112a3 100644 --- a/src/tables/GSUB.js +++ b/src/tables/GSUB.js @@ -59,11 +59,12 @@ let GSUBLookup = new r.VersionedStruct('lookupType', { 8: { // Reverse Chaining Contextual Single Substitution substFormat: r.uint16, coverage: new r.Pointer(r.uint16, Coverage), + backtrackGlyphCount: r.uint16, backtrackCoverage: new r.Array(new r.Pointer(r.uint16, Coverage), 'backtrackGlyphCount'), lookaheadGlyphCount: r.uint16, lookaheadCoverage: new r.Array(new r.Pointer(r.uint16, Coverage), 'lookaheadGlyphCount'), glyphCount: r.uint16, - substitutes: new r.Array(r.uint16, 'glyphCount') + substitute: new r.LazyArray(r.uint16, 'glyphCount') } }); diff --git a/test/data/NotoCoptic/NotoSansCoptic-Regular.ttf b/test/data/NotoCoptic/NotoSansCoptic-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0bfae4bebfa60f9c911d1f103622775efc400eb2 GIT binary patch literal 28084 zcmc(I2Yg(`wfCKSSG&@xu69?eveGtbSH0|Nch#jWTJ>V}W))YtNwVc`(`>L!3&ymV z4h9}1fj}rBJVH%Es7ZhTj}l56kS~}dJU$@S-S0nh?@F>EFE8)+eZSB8-I=*l&N*}D z%$YN1=B_!yaa=riE+=r^O~J;H%BXyo<3`^@?o3m6SMOa{=G?<^W8EAV=Wpum_p5GM z5IJu8N{)-l>gp|VUUcbuog7DoP=9jumZ`aOu1?A4IK^kkdv*QhJ?nlR{sYRQFX6aA z@$}lMHF5Xo&qm$1@pz_@p?Hp5jk-F#XHRe0zI)r9f1bhn!yG5nY@S^`_3h~+2RTmp zbL2g>Woq{v_ZRLwq$^P0JTtXr?KSs25#+eGM^R^dZgzgVBkkc5j_WaUT+CA(?HiA$ za@Ca+i8cSp3BreX<9;1`atn=LKly?B)E(g@;WGuruVi&;0oo~w#J3RYPu;QbN8vLD ziIznEJ;rk01K*NZ7R7Y>KLx*n9Oa@wz(qXePMZ{yan4En$c>HS6;TSFSDed{r+Lmy zTQew4U0qG2f#bqqnT8^g6Di|U`R9-}qx}#!jub}}^5f@nX`182GCbgSco4Az&ofBB zfcUSpRC+)44EGMFqmLq*s})K)FMk(T#&1G=F*n6qxqd!}bMWz;6J<&K*MQq8*R+n~e2G<_1uwmb|w_BkuukPK93+vN;$3YtF;BatdCZ_6APH+ru9*e%;Udm5cInvX+Y_TRAO182*Zn!Xx)@k)LB+IX{Me6oOx` zfsP6;#J|D$!RsQvh0A5U`!mMl4UF4*^zlKA+k;%RLeF^xC(66gC(6sKxPHh%E*>|1 z_&1Ta9`J%(DtM5FCl%>5GQwq%@1ReG9F?ipnfOTZG)d-w=OFORays&@@Ehbp&dK*e zw$DSGA8?7}dZcw+H-8J)O|B0Aoz~?IXv3ra7f4&VEJ1~Kf8nwqdropc@ckb7JCS}c z{CDzqwgL>yfB<5Eg|hW#Bu{v4?MLi{2;g3>`+YvGT`J#uCq z`ax%kfG1l{zlt|H`?3+M5xzwhjF%s<6aA1L+W#W?q5Qa6 zexS2xkwEJz5u;2%9v6dHg@C)3t=!q%#oR62&$zd^lO&O3lS*XIqu#E*LVc(Du~-u8jqQou6nj(m%_H$!N%!%y=l{J!8DF&UnE1l<}*~g3PAO?U_H% z{4%RF>-wx;XT5JaWV*t1v*~WrBc^9eubSRBePa5%Iohl@=b1g`R`Z~Fjd{*|zWH+V zZRY#TKQ+H>{)72%7L`S3$+fsG&6a-42Fp&%MV4zYf;?T*$d_2-1P+qs(pWS(7L8?` zTqk~;xW(uB=N3FJeho)3FMm(ciHi!V*X`2h{`L2r)C>{J_;K+oUZnC#ig}0ImBkzM z;^T&@vbYVgmYhU>Jh`$mS)Jl7*J8pD@e+PK+%MO6*YRGLK7m(>mn7P(u^Z!@m8txA zoVMJXqNc4-KRn2fb9bRWUIbR3!nfJ&4y96cm$sxZek87_Ok0;;k&)p^=f`7pXq@78 zYh#m2GBS!az~}(RZvJ*kIdNO`qJ!k|<4sK{c}A!wmBN}s z?UlxAcb9uy&J?{)nUa;2qBEKJtwv*JMnLyudbdS(edN{POG&ekYB3RmcpncSUvK&XCWQXL|V7UgV91uT}p2J#uUGe&yTZq zTh;dd3|cds%{?3b39V^Zs%h}qs!|*YZ_`R~)-EV8!qtXrh3B!l)WV{*+8yqc6qnQE z#dxBt4jYRo$GrxnQmA7L<&{H)v zka$)FZoig=Iiu+om@ua*Qrh5 z$>V7`aq;$)jwzd9sK}W#37W22=dgE0RegC~pd`OU*H_{(RoMJRK67oJcWm}CrJ`FA z)zQTpP20-rYIY56J62GZyJKo{Wl1v>tN2XAXkK5#=)T;t^zQ9FJMY@;0J`U zLD3@Q4K($v%hHoePJKb4PK8tX-H!=IWIfB&%^}3;+uFwQ`a(y6 z36LG(8~9}YRcJ#hyLJZ?NDvS18dP>Fwv>LVDpZnKW4@5XeMhG>KmJKqd!Spb?oCMY z&jtH8mp}Ble(%bh8hw&)b=7$r3NGB#IvT8xFVL6`HEDsd(uV1pv+nlxwnVG?6!F09 z4DTf$z=o4BJL-7!in2|w17ZoUG%ZwYoGIRco!1!7I#zFR=2z4wcV-lhw^p_nYx}R7 zbbQa&l#rFnC>pHj8)Zb1cI3UA6rMxo`KFDvESfz1i(YHa~K$!Rj!Igmj_TB?{XX}1K6q0aXEI>6t!54 z-|I}vwWOBUXV&hXy7lagJgd0jfA0&GnKU15w&@!k1w(gz@O^uFQ`0?kg{uvJ&i_zI zhBOpQqei*q#nMAp9;h?J;+QENPAC|rAF1XcQ~l?+HQ({A&Z%l$TK!b#p4};Y=GGM> zyQ;e`9$vjwThLy)eyejt-EnOFw)?jy#Uv)EdQ|ZV>clmzyt$%ne{l0?hOW4btYn2cN_}{9a9gltvTehbDCFavEGqF84!4(RKuH;rtnsnrh{E$jG1Tc*`lxOsD-&zfml z_zQ)V6rSfD{IztoGXU1tKmpRpsgf>@%QZbb?Q&_lN~SL5M@!Nwat|EHtw<|bxE)Zi zmmSA`Oysn9an}i6W;e*|`l&ne*H4d`R%LIyw6$jZ!S2C_NBD7df;}%UN$2#Wt92zt zV+jmyp3{f_%3r{w6HAK$>j!aQ_^4#27_?Mm=$eA11q17KU`n1Gm^PB2M`N!j&{lR; zq}-e8Ow*S8^fC55-Tn=>z^Cyw35`|6cp$Gx+mlvZF3xze2l5J%i$gs%1y}z{n+BXZ zj9~!UC5`DXz|UEf)NexhO6#Paq&f5z<%yjuj5ns_r|8N&`e^&Q8utoo?N4Jp>W1Ri zn(aAl>b^CbfUFm^@aR%U90MQiTnmM{hwBjx~n~vEo-+QKjaHmRbPFqracrq%BQAPyUIOkg{q=9 zI818HMRv2eJI`6fG&Ai@Gx=AJqLWHOL{FtcQ!VMKC1E&CM}3rNDlgSmc6bdBC7bNV zg};%tcQXa`mdTdbm0nXWuFuO+Lpxmz$k?6oN1&Zj=r5I z=GcVJ?&jfvX06n=yc@_*t=CoOFb5CqIRp!$yyyYr2iK)f1+~$c|X6O(bUVFJw;q-Q9InS~8lQn^;Az z4f@3yYsgC>&{=gbGOOqw&IMb_qL~^YGb1D7G5$I6Wm3H0AsyoVs7>d3F7UBA=+ax< zsEyr2{}8ba4TNP79$O5C5LCPXl}k@ zZqu>0wqxRV$jdRavvb?t-?mjL7Ad!?$L^S&xovFp_SxCn$H)!a&O1-6KeA`f5n!Qf zq64xT4{rkIt~?uP?}6kD97RpD4Tt7iX6p}&eYz4)^ci4)%Z0Bj5_%)JlBd?gk&^{Qe<6b>Tyqo10J{o)uaj zi?i27h)qzTX;z)-$qnM~iGAZj3)p(IHE4Z9d{*2-`WwVQ$-IrHUN_d3>S?VDCI;Ph z!CF-5`&TOXKW=ocb9rVx8w!RC{k-Ag+*TdYRPU;8yQ;NjPpt=z7!nr2lV;eNuwcOt zhalJpU1VS-!{nrTg03{+JIp*jKBqio{>4e6NKeW)sI@9xf<2|YG5!9;3{A8qD%PZS z)Q~EW_<5kv93`|Uqbq}AL9S6jd{Nl)lQ3P>Uk4wso@<%RMOHDaoPv#+dTWz&%5pT9 zYBSOja*M1fowp6Yrb)7x`P6v3L;QJuN+{Xo{J!`m*_&-kmO1!1I0$`7sBkeg;9&N* z4tm%Lfjn$M@+<^l&Q~f(4bWY5Xc>QbBxwf==MG zCZeZz)~<2a-8B8mE9Y-HZ|;>>cHWU!wcfpJy-fRMpdJ1Oj6QJ{^K|oRBQ}VmW@?(u zD2O18ZV)e5@JbMTUs6N6nOw2p;aAwEz2a6`2I&04x{-nV7sl=%S*d7#EYt=PGGja&7I>`LOWRWzrXIH!{@|y&J>AY}5P1R)8 zW|G}=#Vq*+c~I!+4XsNDk@042O-V^%ceJ^rHoGuCF7PTJ)7=crdC)sQ0iC6wlkV81 zy^PI`gqIE(#iRmBY_6;5&WuS1R&2YiFum3=-Q6+VvpcIHF}cHWl;xjlR4x1FY1ImO~hLsB2Zxu3a6?t(~FP?oWki@2Gd>F7YbzgHW)hh4zN>eHdDX z?(XHih|4LPAhN%J?jtYQa;&B6+L^6=*_IWh^XKo|SJIc8+piwEV{Y^9EA!hEQ?EUA z!F4b^5)%NES{{!>o1X?#wriHbENq&s_it^^YfsHcZ7|;0(OwuZX86=qdnZ=!uhPV| z#3VgGwLUq%6UN5U?nQsv?nPQ`Pv5za(#XbTAKSP{+ZH-1un)1yq|*HcCZxP;kwGl& zSV*3(_fklOopn~kj1chC4U&4KAA^RQ)dcV$*!rdzuvHlv|8Z|(DO z>(=J=He{%0%2rGiIVakShKAbrG?q+sm(Dy)4!HW=gEO-Oo_;5&@POhQaZ07dm{ASu zrZhkWxTGzHN-x0kgQ3Gd8JHcUzH7Jt(sO#dclj^f-#gaVf6qu?|L~_Dsv38#*n3p{ z#v7{8&J}x(#(qdj$u%+SM@Oe))YBs)>!1z^Jq6-JOzt_CR^p@9CJ2f@|7Oki@11z( zlXX9Sf@~)P;)f(v{GNCb@rci%6?8EPGg)V?n9srHP$fV0qcs;@xcWy=uQ_%lNhZqY zo)fwXV+-C7z%#F$0+|lv&Zt8}Luvp0byegudqK zEb4Mfp3dN&u7>s08>;?h7iaaZ(TIz4>&<-fYQ{SutMiZA%n1P3qcRgUdXaIV3~;eQtAM!9e}!Zn!W9 zb~JVEZLIW`*MKW(?gjoVoP9%sdfn8TwdmDE7$j*g4AMi1LCQHbcYu6~Gj3Lg+G#Gm z`kB7x5A>60@e69=Qd#DC$bgc{kA=7itTD!GWU-n|RtpR;p6dX@|HQ~kkaFB!R}^t9htw@De11TDgOuvhZMO8(?Cgc+SN^y7 z?t7Ugv(acaWnz}mqk>^jL(f@Yd26w_QolWYjC}3F)6&E5>*^AJ+TATa(FMy#pgVr- zaHx&C*ou1k>0>4DiWcc1&v$fu(BA%02PJgTqJYLDY!eaLk>t1upCDeiu(Tn$K)jLs zluU~svJJ@?DR^g{h(kldnV0q?Bx-5dx43h1+)?C&_{p6$)jHB0XSIrdCob_%WGzuV zVQQ>)IO2ovA0PQSg`wtc7{$Gyo%L4D&-1oHY3#^;jNPvmOUNI{{(g*^G%PBhe!lDe?)zZqQ>D$GX7Ldck{;SeiW@s78Iqo8Jd^Ix+4tM^JS4x%v_6|wpub}C4n38qCC|iU%J~=7d2>0JFTL}+0@iEL! z*xdB6Y$zz8rEp@S&_QkuoQ-miEE9gqvb2i!K<>YGaYz0?INKpxXDCdU*vo(65k&RTzFw1bgKS;a2SO3i=74uGcqqG|7X1TSI&)8-^>Zczj&M^ z#6}npi6e~o_+LM0g5=wlbZ(WUJ@T9 zkN&cLbD;hlwI-z}C9S8uE#!NyyXkq}IoTB0P)%o4LfB7!3F(Eu;WW6gcuoWSc=d3m zd9@D#na{R64+4{zPkLlvO<1N?L*0Ga4F_vBg#uecP5Z7>_t?4)Om5w`qQcjj(4MeuRbbLxb^h%9 zZPQNl)D_kMx|K7Z23gn;Oo@vtzijT_{XW3AgZdOupLd4DLEt1~d&%uW9Xfi(($A|l z6qU@DO|ITPy>4pbhVF*IaDGAlzK-p?$2vyFk4p00><`wpHPv{ktQobYf;Am2{c(!8 zp{DVjz-ok?zl>fL{Bx{Sb<1d#&sO(mng=RI`bXx9iYI$(D?Cj(j_j#GXr!R8c4cSv z4oUvI{B@zWP^GKdl3tTlP}1#c>55gvPc+o6p))~+Rfk&P@KPD5%LnkW-b8u}PuI`3 zu+3*~7TtViK6FCZ!d96My26}#561XM7!v_LaqOnx;bIV$12Ftx_S`*(4%*U(C;Gar zCr%2{>(;E<8mjGR^ey=4ET?P8-#|f>v;tf7@Ksh|E*|QprC3$TEtrjRsSCcz&-hfa ztOaGc7_CG})}%cvc1wC|uh?cx1|Kng=>-IVVaVOHuW`@uzQCSwPi|9D zS%bNty)2`k$LFZnpgw-_+;`@yR}_t2GO=P+L4LqdR-F^yqBfOvR@P3}Q%tepm86{i z72TQQ9F6*X40_4O=h9qtk?O!Ihe4gCYiNu<{{(;0{H~}LUg6GLpff^?72d{YQjalO zf&p*{vZgAR=J+9H?MhFQB}QpZo=n;QDer8bD(2x#6uK8Kg1zxoc$lvP=M$-S4jL9) zJ#2ZYiEYubgGX#au~*t!SJ~HkTXS(uBfl3z)Hc>(%};P@^{`+7 zZ=8IJu{skTwBT}hGj*QfF4fSz7kLpbT-b!*DLb{+VI6Tm#?ViCZtZn=4R9N> z18ZVPF?ivWbqL@dOQ@s+>{Sqa0woCy= zQaVU#?@R0H@l8CX&$r&g$C~5Z)r|&E5=jdheeR*$9(t;DF)oc9KPb-POlc>ttxTKS zmKSfS=i_pc?CW7!GD$ARUA=4$E{uqO2MFa8nk7F^ia}bdC7+ms&7~Al*gjiZG3Kso zca7CHY{|9ejC!hFminUViOQ<^ffa*Y)usLA`AzD|bw1xnwZ`1ikRQm+*0<_S#pzkO z))ZsoM9{anzH_KCxWW`)*iv2Qw}2`S=9eAb28(oZB%gq@h>Edv{>3)yY#K*s!h{1! zO>_0$iRy~r`i{m`4tq|Yt0+Icx@>gn!mS-WfohYl!nQ(PzP71h(x)+ZcpICnx<-9g zo-x&)mr=H3aQE3Q!@a>ilPa^M$>r~pUcL zI7e`#s zxv&4klf+r>yG_yCQCUMV=VDc{fYT-}AG*n%EhApSOkL78X#rLObp-dLB(Az7uc@Rw zVBR(T9C`nVvGpxC>`pT2Ydf+}c4fU^H<7X@zqzQmwV-!v^xD?e4aW1<^j^M-*c0rw z0}6kYxsr`@xQ>qncBr)|2W|^U=M_qo3s*(FE|r!fl|TCZ+QC7AD3qybG2dPPAAfkN z;l|&%advliQn@xd%cOqr_u_BR4;?s2Pn=3P0FH7)$((j%!MwF}u;6!u-FDjPg(|Y+ zgS|_Xg-r=0$`IS4aU?576p01KSGR8o&em_pDo)~ul?mCgt#KNG*ZK0MyF;n9-fbs( z)s4P9Uv}QM(BZ?urda;P;l8Fmn|Q0H)qD7G*RBjlAiK6Pc7X5lxHF649OGd}uH^3k ztp@5Z!D>QpVJ+K(!tsbddB+Hv{<41PahZrGhNdHE$=NW#_mY2O*lNT>#3>$F#G$}B z?WlkW#WbOtdQ+k_kfCK*u=p&)vADaGv;l-mGNl)HnrGybeRM-EQ-zs%vrJZ)YhF3o zlHt*&Bo}GMiVHHzQtagJujid0hnmSvjP~+oaj#7Hso(UEP7K7yRH|ZkSJcLv1aksMUL@YvoYSP<%j6RGKm~O(vs} zl*tM4C?EGuQ$u}YV|_zYB=+Xpv-9$@?fH@TUO3Dof7vyVzZl$-No2M(jwUqls6&Af zjcRK8p~prxJorS^wbzo%j>C`jJ~81QK&JTH-5>o;w31goko{*3kbor^aIkCOUzcF? zuRC^pWZkjrVs`H)?YsEYmtS7^@E5;WbKBimnObkgE$UppBK$P}0>v*tDj+G$)}rg) zydmRNZ?1=hd_{)7%tarxar33&My!`uQ;F;^65Zu*zs;B4R#A>m%}%Q!$X=zBZ@*p6 zm!kYOYAM2wxIt*5_aW)cWVXtxSO~oe5}?JH-e7C4jjdv~-$o_JJBy38x7<5&>*kFc zN4#xa^YsS~b!F&t1i?>8iQVhF^{Ta3_pBHpmxzZO%4+A@_wH)57isO(4gkhd{;Sh4 zVs((WPu6e5_^T*Ga$bo(UAgy2)4{O^hf1sJ`U*F0D#~St70Xq#}_#`vf_{s;uB5@EX9KtfO9;hW#wi1)WX3fy8OUuqq zOV7#4b&`AXv(3i5L~(~XJ0~kECmY5HqZS?^N-ZFsPVF*?2_CfWs#{XBRwNE6@elx z!)P!VGe9nGd6SdaXxzna<-@peyRd)I|F7IhS7ODqjhxK$DhAXwPCsVR>-c(tTlVe4 z>yd9CkUNev`zk^MeHqD@zbsh#S_0n`NMB2YC&PN~<}m%{fK$11^f!}+Keg;;Cr9uj zkUFx9dIRA7m5zuOuj$)#>r09g?RD9q>`+P3P(yQ!CSDsF$S=vC9Lp&#q363!%&Zlp zky(ARk%?O~bQ1tz(%BtNzzjuC^*T&%YEV7t@HwKR2BTCOW1_vVuh5ZYRMOYWa+?zs zu|c(UpgpV8WL#k?acRd|M@L)7eC6rgR%>_KPYS)I^tL?c7S@7pY&~6qSL+3Iy4<-E z1fZ}xRKI?*>88E8nWOnl6XvNTck6hCDNvQXhcuUnpFLlcU7KsH_gG_75`G4c$Q$7k zWK4pk!tHFYq=lr*aMU@a)%Tp$rZD8C8B%Oj7F>~>I=r&c(7UEotF9zRWD~yHq&9HGo)TSh5bg0uSit-y2inNZJTD7V*Iwn2KV2lr?=bDY0gvKPDJPuXlcU<9_m}MN$ z?f*6IfzYOs_^NX%JN_%|Fk~7sw7Rch&k8?Dy3lXxhjY}?J1>$;iD~e~n@?ExC0|&A z;MHZd1PTJqDtBs`)mfHVlpL4pHuX*xmUz5*f%c~QMnRLAmyw$hiqAA;rN>0KsMIwz zIqrCcqR!^1DKI8MYbA!iBp2bk&_sGxY;bt7Zpf<%Hd7AyQVjiG#u)GGORi4$*V_9- zIaz_S%uKyKO`-C|l4r!o`)q8e4PQMju~I(VU~E;Y45e9YX}#q%A#aLW>WoreJZq__SS zmq(ZWl3ZP%D!w^4ojfP@#KnbZP0Yqw)WjEmO#awP`d`k(cQF(Dv|0aX7Lp$E?te4~ z!+#GSBnL3}!7yN>&O0y!YbiacO5iW>*Tl7`tZBBqzOe~aiT^CWOP89LS4eWgp)yYj zli5k8JK$17d+^r_Zqn$VYY>$R(o8Q@(A)R6ALeB0wfgKT>p#>fYLh!I`M7h!lvo|I z+^kDV%E>YdIlayQSj$-QS~XEI>ZDLiToh=fdh#vuD7&GIZ`r6qlbn?*yub{Cny~4z zJhmiVXSyy$=N+vW3T?k6)X3Xft4u+dn-jK@&dhkv>dtL5ljpT#!-l;kVEz&?sizCt zS4*#kKnK$mEJ0!OvU%jtJ#VZ?(|Jl~rfQ2?hsV0oD+|ZE%JMBCQXuf5qMrc;Rd1xh!D#ioA}!r=)S{u+gOj z*ZMHcshY<(5EPFr6}b08bAa)_!XObGSdpHiyMh3e;_yVvsX;@fHSa&J7Y zDGC^rC1s$(vXHZ+5C)Ac>AJ>>xU@iHbxxyeD5t6lnqe-rdvwr3MjD#D89IZz?7D>b zrf6MsN`9cMxYaEw35~NlE9ULkI43kkegotJ{3Y*@cjz}EbZ$}^rd}5Zyp@a-5LDXf zMN$2hrz*en(o}79b7Q?mSL7Nm$W2f2H=kW(TrpZ$U`i`b?@UODRwbNcpUcWm)Mp!A z27_9y%+xf+>Eqa(`JBHBdLD`);umDF4fL{+e9uT)Rfw`r`bO`M!?qTuZqRouO70xd~E9_atsDVA2blE|*@g z=*$0edx3@TBy~Nn{e>h|k(!LmijZG?A;e@3?ViCJprO*t_Ehvc9l7`V)9zfvY}`^Y zwxxW-?iSB=iK!-2YKqxB+Frk=xiGprWwN*26?!Y(n3@2SE-gl+BHMJwTExok1Ig8jGhDB+6l`dAa1#(-%3dA>+c&J zBnej2M6u1t z7Q1LJ`OIdqSr(o(+pHG8i{>vJH)m&?`7T;>;aQ8-Y9XI3tKYc@W;<=qUj?XDNYIxx zR{GDJeTcnTEmyO5tMwYW{BWe6*=(~|&9*9(gFWF9{Hnqo()Zv3_C$A(cJ?_q_EBHp zOY!JsUy6UhCrMw5i$_qhds)d5{!_VR5G8jlD;X4eq!RqTO+BBsRI>0#g;Of||4+d` z{3agXoTERL!VjTKnb+p@Z_b5dk#ElBdWTRCz8&eSbfF&K+w(H7&V&GtEhK?!OAsWU zECWnb*ivERK|Dw!ik8EgU-Uyn2^Vf$GrpO;7OOq>|Mg-NV7I^fYrhvDbAIXf0_1v2yMNSRyMOQz5Td^ZLFVH!2vWTz z2&f@J2;&=0?v4nAGx!(*06wx>ti8-f)|~PY6xeq2EMN_|aTGc|lIc;J2nem(yy_&u7W$Sgwwrl+$te z-KHDmbUc4Gevv|glfacn&2a&&0(0CRZX34&E5bCl9TYo3Q5n_>GuDb({H^CUb8GR| z!p(52@wK#>^C4$5;-1A;^XzRcH;vIssOJjzbcWl4!?J^4rdcy{FVL>Dc``jGzDt51Je}vxR%?(__GOl zvw;7P#-)Sxf^vKh##9lq*V@HPdk%X8_7nz>bgZN^z0 z<=1jdl**;{+vWDfOro}fnkrZyC9pT>Uoo&O$yG79ISX7R$XyHCOJvBiNKtwGdh0D` zU|)hTkJ>vxCmqi|w3c`oV)dzH_ae0pb#^g+QVvNqHY28EN+ne4=MJQ4pSGdSc0i(a zdocsr5qB}}Qh8Z|+vTE}_!C12L5-N++ES+dv29Wn{Kf3TaKsO-dat-HsfpStxgD zIh9kY%eLV?GKcmckLmz3LMknhIY}w{XYzLDe3%EtbY$m%bsn-oY1@pL_HjMrpK>0~ zU-(4Wzc7BwO-h{+z6YC%CjvBk5>e1oAfh7CB!;L-EQurWxYU|RlJG@RGSLzp(UTNn zAgLq`zuc5Tj3kp}5fd>J3$YR#ew{0a*l~=KOY%rQDIkTUh!m3&e6~|YoVXxcj!z~$ z#7iphD_&Kkn$(b5c=_u|1Mv|*36LOZBq7p7nn??3C2jazx`TAWgWFA3kRH-Y`ba++ zAcJIx43iNu3h($zGEOGQB$*&Z0PKsLhTy_swwGh~*`k*#DKnJ3%H z4ty}Ri|i(QaJOI|z8N@+oK4Ol=aTc_F+51lCl`=IICVKfE+iL`i^(PAQt~Zw899pI z96Ls?AXk#B$kpT;@@;aQTuZJa*OME_jqpm`Ol~2!k`v@Mayz+${2RHG+(o`azDvGG z?k4w;?~{AUedGt^e)0f$kUT_wNFF9XB9D-NCy$aJlgG&8Rlkxjx{5~1K zPsZ<)@%v=_J{iAH#_yBy`{n-oW&C~_zhB1hm+|{${C*j~U&im3@%v@`ei^@C#_yN$ z`(^xo8NXk~ACU0}Wc&dce?Z0`knsm(`~ew%K*k@C@dsr50U3Wl#vhRJN8~La;}6LA zgEIc0j6W#j56bw1GX9{9KPcl5%J_pa{-BIMDB};x_=7V3pxpnUj6W#jZiSaRx#%{*a76B=i`6B*P6cxS>WTqZ={f8)A=gM_H+}u~dr6q{t~nE-5OPBDWOD z4ZPA@g%njvkxz>JQWTJ)pcFMqQHVt$313LU7n1OWBzz$WUr53ilJJEjd?5*6NWvGA z@X38IEt7cW4g}(Z!TpNR9-JW-V4?AWo|!L7`dNNIMxa&5M=umeQ_JBu&SLrX}FWUj$BbSm|O8 zg@c^(Wo_LwEzyd$OSIA~)_5sv5yN617cng6EJ9l9&?2OzEUAycx2%sek0D%^Ck;WQ zPxNi6Khl6K(oA2M(Yx4^QMTBUy)Cws2wH3@y-FjyL>qlyP8aMof+vpIZSAn<)c9&N z5%wDW{W0n#Fe7c@E<^e#cMRz(xGRyq9>1q5a5uxZK=4bfk0Sgr_bzgH+&rW0rIu0~ zskT&u;-Zh5E9W+Hd@$7B$0e+uo!gG{1AJ?ZUz7|31_7j{H)>@nwoGl?#HGvOrY)Pe z!YwRB6L|N~f@f_gcIx9$!5a|+EGhUB=Vc*D=!;5Gy{7)Q`jgl@8DxTT!o{gxMwrh{ zB8*2%sZ}hyn`TCTFGr8B=Wak57mIUC_LvdlpdgmUIFX3ONnk8&-&kA%i>;F$0W{#* z6LpIFOv!U!DUXq}l{(U=j3%6N6;Z0nNu0`v_lHOy(v!+*#CS)VKFLU*jl8pA31`W9 zn~|4|ynMWuB0itiLE5vd9pI>p$lHeU8bvGdt8&RC-svOZc?h_IiXeNA!LDs4oyu2{ zckW^xsUA;8q;{%Y<(p)k@+Cs?pRFn;=PQpX1{jVY+0FU|T9iT9vqx2GRS!qVHT)>T zH7uNAVK)C=@y-}`U`h#PdBZ29RvlwsBVbr|}>c0M*i(Q{taFPM` z9K&9$n~VwY7qh>m{GgPNddEnRJx=T+yHTIVnFrdjJRSCL-6VmgX;}p3V$9%E=nRjJ z;aKt&z&}8B?o-^aG49ZFQ4joIefDslkD~7pP9dP9D&ao&?Ty*r$JvJ^41Y{+x!WN8isz8GSCoDf?0EyI;V5`&R6&@8N@> z3=j6xkDv{u>jlIwV_*Cm#J?jS{T@KP3@M?<1!>EE*SAr_Xg(8%%^So#z$HEze%e7cCpq6tD z2-iMpjYg{E?OKID>v48~EJgSQEihH*@X zPw9Nphw*gM7&5XQr%_HwUk%!NveeOeKqc;a)Ho$SA4k&UR^HAYNFtR$N&aN%n^}@RsXio6l0o~T6grp7A>Yjp z@MHXTeic6rI<`Z${NN5D_SZEF7 zmx+i5zxa*yJiH6xwG6^B7Vc%?XRPLamVS?=YguS#ISaUxOYpy*Q0yDR`0W&eZ$c5? z#KMs6V>0}ymU$O94;bElTXX%$&>7nrVP_vfxqJowZ z$wvHDcs0_n=1~7j7Cy~Z*{8$!4RvyewcWr%CkyXp;Tjg=w|G$pRvtp!yhL~nS zw+fcd4tFE}dRAJ>LL8_fr=Nu`7S14~Ypi5}uA(*w_buGfhc${-JYm^7zOnsh`*vM3hrOiuberW4qrgS1tkqnG22WuusToSSJ_qg#ufkBz~UeMHT!r* zIkmha{D@=xNX88_I-{|+p@hy?`j7q^6H?$bP18L@DSYe8_Ymn!H_$xaL zS8tiaPJ@NWmgHw?&k)%!P}@HqqmT${HMld=jI)jX;N!jAbKLJZ5jw&Nzwr#YgIDwI z`1O6AP%La04hTnu8-?!)PY5pwZwns_Unt@f>55#1M-fyEDJB>DCqNc(2UOSy3TO|K zPb)#61>AaNCEC+UFi!-0O3X@hr{oNl8ogg=r_SHDNE3(4awtV zvixf`-j-|7--nHdL>EFjE4Vst1fxz*gty_GV<&g+*V^4Cx1%y-gFTbNQH$P!bS~~{ zmq2RhiD(VZjT#}--8l6b;9lgOg?0Hf_Y>}?_`g=35)4=sCBazDfy{f(5R)^*^=(pxF` zv-D;Kzn0!~;M>xh5?08 gL$d!BUPYmAYS5FwU|kRDc&r;w!uoj)J2;g5A2KC<`Tzg` literal 0 HcmV?d00001 diff --git a/test/data/NotoCoptic/OFL.txt b/test/data/NotoCoptic/OFL.txt new file mode 100644 index 00000000..ac06190f --- /dev/null +++ b/test/data/NotoCoptic/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/coptic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/test/shaping.js b/test/shaping.js index dc005ea0..23878b09 100644 --- a/test/shaping.js +++ b/test/shaping.js @@ -582,4 +582,17 @@ describe('shaping', function () { test('SHBALI-2/12', 'NotoSans/NotoSansBalinese-Regular.ttf', "ᬓ᭄ᭅᬸ", '23+2275|162+0|60@0,-1000+0'); }); }); + + describe('reverse chaining contextual single substitution (GSUB Type 8)', function () { + // Noto Sans Coptic's ccmp feature applies a Type 8 lookup that converts + // basic combining-macron glyphs to their `.cap` variants when followed + // by another macron in the run — the spec-mandated reverse-direction + // pass means the second macron's earlier-stage substitution must be + // visible as the first macron's lookahead context. Matching HarfBuzz's + // output here proves Type 8 actually fired in the right order. + test('GSUB Type 8: Coptic stacked-macron .cap variant', + 'NotoCoptic/NotoSansCoptic-Regular.ttf', + 'Ⲁ̅ⲁ̅', + '33+633|196@-319,0+0|34+574|199@-291,0+0'); + }); });