From 45386a972f5a6b9371526fc22468250d7b8be2f5 Mon Sep 17 00:00:00 2001 From: Teo Mal <76534902+teolines@users.noreply.github.com> Date: Mon, 28 Sep 2026 06:47:53 +0000 Subject: [PATCH 1/2] Apply `dx` and `dy` in logical order in right-to-left text `dx` and `dy` shift a character and every character after it in logical order, but the shifts were accumulated while positioning the clusters in visual order. In right-to-left text, the first character is the last cluster, so a `dy` on a `tspan` only moved its first character, and a multi-line text collapsed onto its first line. Accumulate the shifts in logical order before positioning the clusters. Left-to-right text is unchanged. Fixes #1093 --- CHANGELOG.md | 1 + crates/resvg/tests/integration/render.rs | 1 + .../tests/tests/text/tspan/arabic-with-dy.png | Bin 0 -> 2266 bytes .../tests/tests/text/tspan/arabic-with-dy.svg | 13 +++++++ crates/usvg/src/text/layout.rs | 36 ++++++++++++------ 5 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 crates/resvg/tests/tests/text/tspan/arabic-with-dy.png create mode 100644 crates/resvg/tests/tests/text/tspan/arabic-with-dy.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index e13b8f1b0..8f903e379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ This changelog also contains important changes in dependencies. so a nested span stays on the same baseline as its siblings. (#864) - Panics and incorrect clipping when a filter region is smaller than its input. (#1141) - Incorrect transform order on the outermost `svg` element with a `viewBox`. (#899) +- Relative positioning of right-to-left text with `dx` and `dy`. (#1093) ## [0.48.1] 2026-08-02 diff --git a/crates/resvg/tests/integration/render.rs b/crates/resvg/tests/integration/render.rs index 79c9e3c79..75e6c927a 100644 --- a/crates/resvg/tests/integration/render.rs +++ b/crates/resvg/tests/integration/render.rs @@ -1675,6 +1675,7 @@ use crate::render; #[test] fn text_tref_with_a_title_child() { assert_eq!(render("tests/text/tref/with-a-title-child"), 0); } #[test] fn text_tref_with_text() { assert_eq!(render("tests/text/tref/with-text"), 0); } #[test] fn text_tref_xml_space() { assert_eq!(render("tests/text/tref/xml-space"), 0); } +#[test] fn text_tspan_arabic_with_dy() { assert_eq!(render("tests/text/tspan/arabic-with-dy"), 0); } #[test] fn text_tspan_bidi_reordering() { assert_eq!(render("tests/text/tspan/bidi-reordering"), 0); } #[test] fn text_tspan_mixed_font_size() { assert_eq!(render("tests/text/tspan/mixed-font-size"), 0); } #[test] fn text_tspan_mixed_xml_space_1() { assert_eq!(render("tests/text/tspan/mixed-xml-space-1"), 0); } diff --git a/crates/resvg/tests/tests/text/tspan/arabic-with-dy.png b/crates/resvg/tests/tests/text/tspan/arabic-with-dy.png new file mode 100644 index 0000000000000000000000000000000000000000..2c3f253d7cea2111eb5f0f8375a9eb0eed95ddf6 GIT binary patch literal 2266 zcmcImX;hO*77d2QBZf_a1A{`NAR#O^AUH8Li-2ZfF+%ttQ9?)>1q21Op@k4}5J`(B zqyw@FVniScUzC6-i%WzgECwa0(1HkIRa=&!L6#2f%>S7mQ|DIQ^KQL*=l*zgsY=S@_Fw7w@0tBLFyFNEURf=CZ~D*k^AojmSIRbA6Wg5ME$h@Saw zH~)YzO5CY5=2iCf(wf?q*6tUBL!W=Yp7izu1hUZxU^mh7%hnhPCqFxI%J6@XR z%N|-4ZIm?xm~iiTl?d!KL=H%3MthDWF4s21&3t_o(!TrOC{s|_v`>CIaLmZMLsWb~ ze^~EHjN+@?*vF?g1Fb?D?N(5=c~&N`m_JzF|AqKQF1~)$usTREZqN=#I%=)~$g2X# zqKXyEsHW2f9I7$W;-|?tGK(jBRs7KRJR6~?z%HXMnyWL(+ntD@X!$aI1FqAd>hF&w zpAC`F@fhe;7kdbQ*$><~VO3$9CbDRcP?(xjht&UA7l}+7=uHn6^x>KBU2}{7=t+$z zX4qG7IY05uKs0ZQYkEu_WbWK^>4qa^%_aVRt2Dm~S)Jb)E~QmC%iW);)qL0ZJgo&V zHp41$b2B0#DYh)fa!aVbt0x#HC|TGevt-vFc4$iou_+Y|m;Gj!3;mncF>K#cL6h^ONtq>z_#UOvw)|4~`tO{GEDnHThUJppQI^HNwiBEy_myNhDedmdWqEqppv@M zIe7L=+cW9Cg_lKXQzqjd^}!_{<@C?gH731lp|RO2F86Gd?j@d(%KL~^_*1HU(^hzu z(|ZAXPNfk|noa_-`LfrI?FAEUx1f_BD2o&&CN$jgNe!v<7dTBgg7L<-Zr7~lw~(hG zQ@5+m6K>6D$keyn(7QCBgs307nob97&~M`73+5d?Ry(uNBec8f8|xvf)_`$B@fSnK z-WY&Fm%?qcC(i1UIuF;PkMKFaGUjIs4}Q6CbRe?c?WzEN@Ui2L&H2SAgeUxcMW)a$ z$N85|aE8mnoktr`35K?q`WpRHYh&@+6nZRVX=;(+9~ji!WsnjZ4x~LuNYpa<)dfjx zBkaOi>Lzohlhwml+-( zW{{v}^c0tGAYKaHG;%uO9U>Qs&7YyDc6Zx*!FICd>vSAcx8T5c3GfkBknW>ICyAE| zDtHe}H?5!Px072d`V4ln;Q8SXwa4L8=h(3%YvP+{s%z=vF^F8>QaK0DB%EBKsVB zsJ6Nn2--1-E5g$=9tA?wo#L#fx>l$eKFxg&hk}sZA(WNl$u!AlBbnai6y&Jr3T)(o z!F6JXi?=0iakbVWlYJ`_wOHF2+R8HCXpzH_5j??ue)c`r!DKXbzgh6K>uTKz%Vp=5+gnZhHBe(TGcjR5G@+j%wp$H{j z=??JaV9B{N)<85yncC~@JKEH}WycrZGvTDHMg1~WOy9smR{)i%k-h0bg0V`2lS8p1 zh<;B*rZ|3JS{pMI6=jazhJ{{MHUM`2#501O7Sy=|=jD=caJkSdW=3+NIOnY`%56_| zuLrofb#@WUL{A(a@fS_S<&ipH4UIGa!>HRMm5JAi9`O*}J>%Y2vg&emeM+1%&J|-A zws*X4ULX{XW(g3AH2zw2<`oQtdET74P50=#mLX3#dBMw*CQ-fDM#BG=6s{q{e+9hG e^G4(c6lj;Yl{-J%HR-heL&13adEND-r2h%u!W5PO literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/text/tspan/arabic-with-dy.svg b/crates/resvg/tests/tests/text/tspan/arabic-with-dy.svg new file mode 100644 index 000000000..d90e39d70 --- /dev/null +++ b/crates/resvg/tests/tests/text/tspan/arabic-with-dy.svg @@ -0,0 +1,13 @@ + + Arabic with `dy` + + + مفتاح + معايير + الويب + + + + + diff --git a/crates/usvg/src/text/layout.rs b/crates/usvg/src/text/layout.rs index ae22028a5..fca855467 100644 --- a/crates/usvg/src/text/layout.rs +++ b/crates/usvg/src/text/layout.rs @@ -552,23 +552,35 @@ fn resolve_clusters_positions_horizontal( writing_mode: WritingMode, clusters: &mut [GlyphCluster], ) -> (f32, f32) { - let mut x = process_anchor(chunk.anchor, clusters_length(clusters)); - let mut y = 0.0; - - for cluster in clusters { - let cp = offset + cluster.byte_idx.code_point_at(&chunk.text); + // `dx` and `dy` shift a character and all the characters after it in logical order, + // while the clusters are in visual order, which differs in right-to-left text. + let mut logical_order: Vec = (0..clusters.len()).collect(); + logical_order.sort_by_key(|&i| clusters[i].byte_idx.value()); + let mut shifts = vec![(0.0, 0.0); clusters.len()]; + let (mut shift_x, mut shift_y) = (0.0, 0.0); + for i in logical_order { + let cp = offset + clusters[i].byte_idx.code_point_at(&chunk.text); if let (Some(dx), Some(dy)) = (text.dx.get(cp), text.dy.get(cp)) { if writing_mode == WritingMode::LeftToRight { - x += dx; - y += dy; + shift_x += dx; + shift_y += dy; } else { - y -= dx; - x += dy; + shift_y -= dx; + shift_x += dy; } - cluster.has_relative_shift = !dx.approx_zero_ulps(4) || !dy.approx_zero_ulps(4); + clusters[i].has_relative_shift = !dx.approx_zero_ulps(4) || !dy.approx_zero_ulps(4); } + shifts[i] = (shift_x, shift_y); + } + + let mut x = process_anchor(chunk.anchor, clusters_length(clusters)); + + for (cluster, (cluster_shift_x, cluster_shift_y)) in clusters.iter_mut().zip(shifts) { + let cp = offset + cluster.byte_idx.code_point_at(&chunk.text); - cluster.transform = cluster.transform.pre_translate(x, y); + cluster.transform = cluster + .transform + .pre_translate(x + cluster_shift_x, cluster_shift_y); if let Some(angle) = text.rotate.get(cp).cloned() { if !angle.approx_zero_ulps(4) { @@ -580,7 +592,7 @@ fn resolve_clusters_positions_horizontal( x += cluster.advance; } - (x, y) + (x + shift_x, shift_y) } // Baseline resolving in SVG is a mess. From b1ce728c25c1950c09adffe272a082fd7cd3b5dd Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Fri, 9 Oct 2026 13:42:16 +0200 Subject: [PATCH 2/2] Anchor text chunks by their actual extent and fix RTL shifts on paths `text-anchor` positioned a chunk by the sum of its advances, ignoring how `dx` and `textLength` move the characters. Anchor it by the actual extent of its characters instead, as described by the SVG 2 text layout algorithm. `textLength` spacing only moves characters, so it isn't part of the extent. Text on a path also accumulated `dx` and `dy` in visual order. Share the logical order shift resolution with horizontal text. Text decorations were split at the cluster that owns a `dx` or `dy`, which is only where the shift changes in left-to-right text. Split them wherever neighboring clusters have a different shift instead. --- CHANGELOG.md | 3 +- crates/resvg/tests/integration/render.rs | 5 + .../tests/text/text-anchor/arabic-with-dx.png | Bin 0 -> 1601 bytes .../tests/text/text-anchor/arabic-with-dx.svg | 14 +++ .../tests/text/text-anchor/negative-dx.png | Bin 0 -> 1736 bytes .../tests/text/text-anchor/negative-dx.svg | 13 +++ .../underline-with-dy-on-arabic.png | Bin 0 -> 1454 bytes .../underline-with-dy-on-arabic.svg | 9 ++ .../tests/tests/text/textLength/zero.png | Bin 1201 -> 1204 bytes .../tests/text/textPath/arabic-with-dy.png | Bin 0 -> 1328 bytes .../tests/text/textPath/arabic-with-dy.svg | 10 ++ .../tests/tests/text/tspan/arabic-with-dy.png | Bin 2266 -> 1742 bytes .../tests/tests/text/tspan/arabic-with-dy.svg | 7 +- .../tb-with-negative-dy-and-text-anchor.png | Bin 0 -> 1630 bytes .../tb-with-negative-dy-and-text-anchor.svg | 13 +++ crates/usvg/src/text/layout.rs | 91 +++++++++++------- 16 files changed, 127 insertions(+), 38 deletions(-) create mode 100644 crates/resvg/tests/tests/text/text-anchor/arabic-with-dx.png create mode 100644 crates/resvg/tests/tests/text/text-anchor/arabic-with-dx.svg create mode 100644 crates/resvg/tests/tests/text/text-anchor/negative-dx.png create mode 100644 crates/resvg/tests/tests/text/text-anchor/negative-dx.svg create mode 100644 crates/resvg/tests/tests/text/text-decoration/underline-with-dy-on-arabic.png create mode 100644 crates/resvg/tests/tests/text/text-decoration/underline-with-dy-on-arabic.svg create mode 100644 crates/resvg/tests/tests/text/textPath/arabic-with-dy.png create mode 100644 crates/resvg/tests/tests/text/textPath/arabic-with-dy.svg create mode 100644 crates/resvg/tests/tests/text/writing-mode/tb-with-negative-dy-and-text-anchor.png create mode 100644 crates/resvg/tests/tests/text/writing-mode/tb-with-negative-dy-and-text-anchor.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f903e379..72121491a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,8 @@ This changelog also contains important changes in dependencies. so a nested span stays on the same baseline as its siblings. (#864) - Panics and incorrect clipping when a filter region is smaller than its input. (#1141) - Incorrect transform order on the outermost `svg` element with a `viewBox`. (#899) -- Relative positioning of right-to-left text with `dx` and `dy`. (#1093) +- Relative positioning of right-to-left text with `dx` and `dy`, including text on a path and text decorations. (#1093) +- `text-anchor` now uses the actual extent of a text chunk, which `dx` and `textLength` can change. (#1145) ## [0.48.1] 2026-08-02 diff --git a/crates/resvg/tests/integration/render.rs b/crates/resvg/tests/integration/render.rs index 75e6c927a..adb80acdc 100644 --- a/crates/resvg/tests/integration/render.rs +++ b/crates/resvg/tests/integration/render.rs @@ -1569,6 +1569,7 @@ use crate::render; #[test] fn text_text_xml_lang_eq_ja() { assert_eq!(render("tests/text/text/xml-lang=ja"), 0); } #[test] fn text_text_xml_space() { assert_eq!(render("tests/text/text/xml-space"), 0); } #[test] fn text_text_zalgo() { assert_eq!(render("tests/text/text/zalgo"), 0); } +#[test] fn text_text_anchor_arabic_with_dx() { assert_eq!(render("tests/text/text-anchor/arabic-with-dx"), 0); } #[test] fn text_text_anchor_coordinates_list() { assert_eq!(render("tests/text/text-anchor/coordinates-list"), 0); } #[test] fn text_text_anchor_end_on_text() { assert_eq!(render("tests/text/text-anchor/end-on-text"), 0); } #[test] fn text_text_anchor_end_with_letter_spacing() { assert_eq!(render("tests/text/text-anchor/end-with-letter-spacing"), 0); } @@ -1577,6 +1578,7 @@ use crate::render; #[test] fn text_text_anchor_inheritance_3() { assert_eq!(render("tests/text/text-anchor/inheritance-3"), 0); } #[test] fn text_text_anchor_invalid_value_on_text() { assert_eq!(render("tests/text/text-anchor/invalid-value-on-text"), 0); } #[test] fn text_text_anchor_middle_on_text() { assert_eq!(render("tests/text/text-anchor/middle-on-text"), 0); } +#[test] fn text_text_anchor_negative_dx() { assert_eq!(render("tests/text/text-anchor/negative-dx"), 0); } #[test] fn text_text_anchor_on_the_first_tspan() { assert_eq!(render("tests/text/text-anchor/on-the-first-tspan"), 0); } #[test] fn text_text_anchor_on_tspan_with_arabic() { assert_eq!(render("tests/text/text-anchor/on-tspan-with-arabic"), 0); } #[test] fn text_text_anchor_on_tspan() { assert_eq!(render("tests/text/text-anchor/on-tspan"), 0); } @@ -1598,6 +1600,7 @@ use crate::render; #[test] fn text_text_decoration_tspan_decoration() { assert_eq!(render("tests/text/text-decoration/tspan-decoration"), 0); } #[test] fn text_text_decoration_underline_with_dy_list_1() { assert_eq!(render("tests/text/text-decoration/underline-with-dy-list-1"), 0); } #[test] fn text_text_decoration_underline_with_dy_list_2() { assert_eq!(render("tests/text/text-decoration/underline-with-dy-list-2"), 0); } +#[test] fn text_text_decoration_underline_with_dy_on_arabic() { assert_eq!(render("tests/text/text-decoration/underline-with-dy-on-arabic"), 0); } #[test] fn text_text_decoration_underline_with_rotate_list_3() { assert_eq!(render("tests/text/text-decoration/underline-with-rotate-list-3"), 0); } #[test] fn text_text_decoration_underline_with_rotate_list_4() { assert_eq!(render("tests/text/text-decoration/underline-with-rotate-list-4"), 0); } #[test] fn text_text_decoration_underline_with_y_list() { assert_eq!(render("tests/text/text-decoration/underline-with-y-list"), 0); } @@ -1620,6 +1623,7 @@ use crate::render; #[test] fn text_textLength_on_a_single_tspan() { assert_eq!(render("tests/text/textLength/on-a-single-tspan"), 0); } #[test] fn text_textLength_on_text_and_tspan() { assert_eq!(render("tests/text/textLength/on-text-and-tspan"), 0); } #[test] fn text_textLength_zero() { assert_eq!(render("tests/text/textLength/zero"), 0); } +#[test] fn text_textPath_arabic_with_dy() { assert_eq!(render("tests/text/textPath/arabic-with-dy"), 0); } #[test] fn text_textPath_closed_path() { assert_eq!(render("tests/text/textPath/closed-path"), 0); } #[test] fn text_textPath_complex() { assert_eq!(render("tests/text/textPath/complex"), 0); } #[test] fn text_textPath_dy_with_tiny_coordinates() { assert_eq!(render("tests/text/textPath/dy-with-tiny-coordinates"), 0); } @@ -1734,6 +1738,7 @@ use crate::render; #[test] fn text_writing_mode_tb_with_dx_on_tspan() { assert_eq!(render("tests/text/writing-mode/tb-with-dx-on-tspan"), 0); } #[test] fn text_writing_mode_tb_with_dy_on_second_tspan() { assert_eq!(render("tests/text/writing-mode/tb-with-dy-on-second-tspan"), 0); } #[test] fn text_writing_mode_tb_with_rotate_and_underline() { assert_eq!(render("tests/text/writing-mode/tb-with-rotate-and-underline"), 0); } +#[test] fn text_writing_mode_tb_with_negative_dy_and_text_anchor() { assert_eq!(render("tests/text/writing-mode/tb-with-negative-dy-and-text-anchor"), 0); } #[test] fn text_writing_mode_tb_with_rotate() { assert_eq!(render("tests/text/writing-mode/tb-with-rotate"), 0); } #[test] fn text_writing_mode_tb() { assert_eq!(render("tests/text/writing-mode/tb"), 0); } #[test] fn text_writing_mode_vertical_lr() { assert_eq!(render("tests/text/writing-mode/vertical-lr"), 0); } diff --git a/crates/resvg/tests/tests/text/text-anchor/arabic-with-dx.png b/crates/resvg/tests/tests/text/text-anchor/arabic-with-dx.png new file mode 100644 index 0000000000000000000000000000000000000000..ab3165e5138737a3e80987e3056a97ac6580c1c6 GIT binary patch literal 1601 zcma)6Yfw{H5WWVIKzNKrqd?I}i&m3>P)!6>&=iab@-T#OfdZGdK_NmEgeX?bB_dTD zv<0HX2aZY+xWWYos33Sj5h1)w!b5@jASw(ag6JR`8fT%Yjb6!tU<~Tg7!!QiMfK)1- ztTer6EmJ&`X(#(@_hU^0rTfCb98s#{7RkYiTW7-?;K@v_@>x@%nSk zox2?bI*Q>;=xFlrd%$qVKY~bj@;j@NzY_Z5_l)HzhN`o)rpT4Pv#`3d1S83UC-4H> zgjYGlMIh(rDV*wY15T8gGKDM5AWgR!m>|J@%JpcpzUaYi9PL1$GHd}@VPb+vo*5(e zLcd+^T}?z|)n1ADnoxVRl7{Scj)EDBX(8PXZqNDbB5S0V)hJLGR*b1vnst~Wf@w%q zPNREOcb$gI+9==DTdN*ksfNk9&9)7tEG|S=Qe(E?E!mZ4K_AItry;LC)2GF?NM zS`ZJ$=h}s6R?&46CzpofHQ6f~O0W0po{elKvbkc6PbA<bKYF8nO%0Uqx)#k*mE> z^QAAeC%_VfVEm&0<${c~W=Nu_6+2eYVg;53#}hOcTEB-U&*;n-fijmz5i1+t!sO<~ z;7r?Y8VE6X!WwM6_`WrmOEE!Mec7u~1+C)6fSn^)jwMyCH#XI5F7UfrdHCmqTt5kby79nqA_ zw)7I#5n&YHSkTEYyIpFd3RXkCKAo|t3@x}E^nb8yG!P6;C)X-zcB!Vc97BSrDC-zm z!x69B;G2Q17P|^k800mGfZ6@5(ol0hRvz%0DQORox4;3ktLX5WhqW!mRuIP0^2f7?ek~0+<7^LPk9^xJ(cUod`lPjY{(Eg;Kx;_QdeKIgSNLhS-ZJ-sn zu8`v1+jZ(mgb*(X`+6Jh6hYHpSMKi5=)m5fJuim~A%dbR$7=4yfA9f57P0_UXsB$+ z-qao(pe8{3@5zRIt>1!>F&)*VCFKLEAfn=?pa6>#crn0MD*Be(X-?n48OFxTv14L9 zD6!717d?sxMk4#kg7z*qIDts(xqel9#84wIN(=FqbdsQ!jaqQZ4h^eS+Vgu0;$WfR zN7wVx0MXX$K?@b@i-brhfpyBHe5xc4=gP1>y`698Kt7sC{Yo{mEAE?6h@zno-ov0 zyDW3T(QZT)0IJQCYqv@$hat6Z&aPp9rtw*#U>Eyb6M{9FsD&24$_bjZ`k3Nd%+v6Y zxqfbg9~<<)YoGnOp0NMGH|`}h2NOL8)4`gZbxA$Wm(p$m(Y1Ls8>@on>-RCh@$~U1 IVnHc?03wF82LJ#7 literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/text/text-anchor/arabic-with-dx.svg b/crates/resvg/tests/tests/text/text-anchor/arabic-with-dx.svg new file mode 100644 index 000000000..5863ddbdc --- /dev/null +++ b/crates/resvg/tests/tests/text/text-anchor/arabic-with-dx.svg @@ -0,0 +1,14 @@ + + Arabic with `dx` and text anchoring + + + + + ادر + ادر + ادر + + + + diff --git a/crates/resvg/tests/tests/text/text-anchor/negative-dx.png b/crates/resvg/tests/tests/text/text-anchor/negative-dx.png new file mode 100644 index 0000000000000000000000000000000000000000..a435b55a4fa7463d915207821fb24054c201cb0e GIT binary patch literal 1736 zcmb7F2~ZPf6#f^woHa-})xvJ05M!cIBH>U#Km-I#U;`=z6a+O` zVdN69G)Q2IDH0(FUN9nuz^Evhs-qaW8X+DO`Y&-hjsFi6x@QC;SF~}R9pL}>c9*koc25oe7v=YN)GX4Dg zl)logS+nNDtI{_%HrCVAQ~Cq~;UmEGSj_|sStbNhx446)X7&_6|KMO2D>61NAuS!t zKT%v-RxfOmblkqvJ3K0#oH8$REd~IU`g(Z;CEtJ9yA8<%OHtu)V@<9+R#*GRx7mlE zP}WWR*mDhsW z9b6rXzQSAs6qjm2XbqsXty1+r7P>!@vFr817eQU5{(bu99jIXW^G^ANFNa>=;OImd z-g^SXB)Y`7^>7%rGK!yNnyIiNfS}R#$O0346~^#$mLZTIZ+F2W8~D{Xz7=+J;T+&G#XSA*_Zx6g#X zr)s2=mN`x+voMqKnmC5ru0-!n0SA6_sdJR9+E_fRv)m=Sc2<*45dC@O?ZPQ%cHs9- zZyxk;pLFS}+UqCuY>0R_UzD)@03ehZq)%%!$@s4(nz2JMLRNbQs%=Q5f41|h9iXH# z^f6JxTa_tAD-h==UdBxsZmNMeZ_o&9bG6lyJfL%t*L^1|fcQ8JsnTqK9eW0qVq5hw zMMEIs#Hv!6a-{751bPD?A0IS?5F+qnpGKvJPisrM>0yY|?x!kK4m1YHqc;Qcu|WfD zEsX?p*omn07=*}Sv;#{iv;pRAf1@0%uIp9 zz)Ro+csrjWDE^2b_P+=g73_qcWDx)*tjID*>${6ey>Tf?%|M(DOLdUm&u|p#!2965 zOM2f&@WjplV0zsdUjH#^6g-I%yamj!%NeX+4O%dn3h8q;R6vGnyiAD + Negative `dx` with text anchoring + + + + ABC + ABC + ABC + + + + diff --git a/crates/resvg/tests/tests/text/text-decoration/underline-with-dy-on-arabic.png b/crates/resvg/tests/tests/text/text-decoration/underline-with-dy-on-arabic.png new file mode 100644 index 0000000000000000000000000000000000000000..106428c71de5cda33aa1c979c470b8a859365959 GIT binary patch literal 1454 zcmcJP`!|#c7{`Y(v~L@ssZB1sd#Ww-x|Pe>aVc__OJj_;ap~32$%NW9 z!mv3?)9|KuP7RI1OFI;MT#gWmSVNf|ZOm}W8MEUb*q^rNoacPbd470)_za5&r!KVMG@4yOff?KZsT5_BiaM}zQ?lTy%Thor`=y42{n&)?{X)vdI5-pCwNakqXcU;7;~L~7wT{n(=Noc zw}W4qNTM7AzzWVqG`G*Fw!jq9@-pe~0`3n}8F2fch&;L%a{uh!hXD@`eT%4nf!T!J z5Nrs+Y-y(>oF{eUw7_}u{zY*xA>uHB^x8kqicD=}qPyere?RMRGa?sG+%KQh10N>a zLEFn;RY^GXr&7z>3teqQflx?WlGA>Wih~a5BNvJ&JhJa*R*9VPh-D|j8tt?BZ<0im z#B-{*>YYf!rZi2SU_B;yKcdg^N39(wM!_x(CcHxXutZiLJEgg#{0e=o^gJr2uBY?x zvabC>(G0feY3q`+)eNrxYJyQzQ%A9*((2{uYT279iP;J?M0~+iK%@X0f#RhE<-E`9%v*FMq)|ByT02ydD zvmw>>T~j_@zFyJz<%#L1^*9?m$-Jt*E|Y;-rZfrp>%J;(yUX~=GOCZ{XEua9A)XGTpV7QW2i&~ zSw~S8&6{r-D0LjoaY!BNFGxSpWQV3^RPlrWK7f(lIzE!Y6VlO|>-LWXPvTR;r&L%>QyO1G&2~6O|+oIY5qq#)-c3N638TDwwfMClm04&p? z!O+P$P`GBn@>O4YUIzb+)I3;TIZaUW!v|GPkXSo`GQDw(s|RPvJS*T@$BmJoNRE} z*|mI-3adc3U5S^O$5;%>Ndhem_R1b&q6$5>@;hkL;ETHZM@`VSn&W>aEk}9vZcfKf fAn+$|bywwvLE)^a9LFEG*=k)suOQE64> + Underline with `dy` on Arabic + + مفتاح + + + + diff --git a/crates/resvg/tests/tests/text/textLength/zero.png b/crates/resvg/tests/tests/text/textLength/zero.png index 1f92d44670619426d873bb30432a3db6455524e5..9c1b32571998a2a21abce93249323d641c3b23ab 100644 GIT binary patch literal 1204 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxVAT=StC&U#m$!3+uU0tY4pEO>um!}^2`*U!$W>zF-f(&1(8Sv!j-&hCF5KR^Gx z!GrzxKNKi5)Z71e2zBtd8SZ)pqsIn+2!dw*swSv-@ZgW41(CpZ-Cq zw$dMb+S9+T%2;)(>Zg6j*82&(GulO8M`|YU9-DPQs?W65R3@7(+aRp-ZN%5t*}SV) z%N*HsH2P)s&$NFczMGqm=CZ7rU0D<0wQ}FAqfr~aDcc>})qOUhcU!_;wlxbQK3B-E zY5%k|)1-HCX2_{v9ckHh`@Zn3eycj`*2Ns7NY1HWHaMNumyX!*>M4iGY}0%>`JjFA ztFGStF0&+c!=;quZzg@qJoEpD@vhFS^-bE(JZqU;80+ezr~21kEuQ=7a#7Kl+BMY? zzO|Dj%7qg>v*TAJ)~@|u=Myz=`j)=XgReGRiZiiYqjqTTmBgwXS#=`nS;E@Bs`o^0P_wVr;JH@(W!BXVY-_A+^e;Z!z?Kj? z`D%2;?TvDa{w7HI@^9Fd`1E&#S?2fLyXg^sSDCN*_*-^qa>U-KjywCVhm;oyN9?ug zu<2e`xqWiww8Xuk+mkOiepzL;&~i=O#V;YQmo=u{UHWCy%QB8^k-c)CO{U3Sx$E~# zOgll=ZT+NAacg$Px?HYpyE<+D;@5SB*8Oh}dbLZdT(*t4J^NUcekM*Z k%XJ5(W_X=SO*l25aq|C@dZKe2eL!V^r>mdKI;Vst06L`Vc>n+a literal 1201 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxVAk`k=6XFV_F+pQvBT%rRp#ezd z(_7Cu>Zi3 zbLTJIxc}hI`wz1=KM-SJU|#F#;uunK>+Q|fvd3-`4HxU5cr+ecd-%+oGr#x8Id0n0 zx9QbPqc?x~B=}=5IxOiuG-c5_tu=jC=U+FOt0^!pUb}70k3}d7YM(uy6M9gS%!MSe{-Qu|e+h^Mte4p4GZ* zuh><0GPd0M`Odep*D78VOus!VO*Q0rPV2O93B4Ouy<>^evtAu}B>DTgt{Km_t$xuY zdiKS)k}0=m>Hj|&T5_=KvrUO{O}%h-L~(>|Sk>FL_f~u8zukR(OW#+H)wy?7d{g1&L@w|1UAd-lEl{ZBjop54Fg@#M4h%aYbO=H$PB_bx3zT}<$HuDwi5 z%zb-%`CJY;0_3Xy|h@umLL7C<*clW^nj#uh8)Rz=8=60`}ME zukZI?|L|<>&f>4vC&tg;aR2;;gn|zS0`UoflYV9W&V#a@_}*xOFykv9C7TA8;libn4#Uxe4?gd0E6Z3Md~+T1>h-$Y z*6Un-^=zlz2Dad@zQy@lFJ4gp>ib3_;{KUsfzPj8$;(md2^m$XTzkJzk9zX_?|2G`yr9Oe#Zf>YoYtBZiZYs*8FwN zMaFDJ&3*f33aq)1>>E-$nJ>a~(yZ9kDf&#=DY3q**G5WKRSQO({PuIo+tLJ6*=v>? zP9@mM^K{vsYAn4PePy3T?QH&<_2x77`r1a6v)Zow5-)VnD)h^ehj$ruH_TD#J-+L7 zboexzh~tO0XSNjM zh$ykwr-D6SGhGY1WN^DVZ0?2IHQQoV=g+)vc`YcavN~e%?tA9fg0kL3M$2ZGJ8y8y zQvYURA9mmD`{l^0^AYQkE3WG0PHocu%XZ9SPx_kD*=xdf;AcQfybUcYr+ zY{R-w+P0Znwb`!u>b-PyK5z6oao^6bUh`Q0bnoF-F5;7``8Nexfel|HxtDyckO{an^LB{Ts5EM7b5 literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/text/textPath/arabic-with-dy.svg b/crates/resvg/tests/tests/text/textPath/arabic-with-dy.svg new file mode 100644 index 000000000..721f088de --- /dev/null +++ b/crates/resvg/tests/tests/text/textPath/arabic-with-dy.svg @@ -0,0 +1,10 @@ + + Arabic with `dy` + + + مفتاح + + + + diff --git a/crates/resvg/tests/tests/text/tspan/arabic-with-dy.png b/crates/resvg/tests/tests/text/tspan/arabic-with-dy.png index 2c3f253d7cea2111eb5f0f8375a9eb0eed95ddf6..d5da3c5044dce517cb4d631b7e06bac8bc03d8f8 100644 GIT binary patch delta 1392 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 o|MyY)MhpKF@uDOMSsGDhT Arabic with `dy` - - مفتاح - معايير - الويب + + مفتاح + معايير diff --git a/crates/resvg/tests/tests/text/writing-mode/tb-with-negative-dy-and-text-anchor.png b/crates/resvg/tests/tests/text/writing-mode/tb-with-negative-dy-and-text-anchor.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6652dd12180183fcf3b99b71a816a2d77d8f99 GIT binary patch literal 1630 zcmb7^eN>Wn6vy!hnmV0f604DwFkiq*PjAB#Q_`4>%uF9ilG)6X`I16vFAmrY#UhoU z7v|I?vv?9PXKLv(?HK0fOM<;vmgOo)$un8Tj=2Zvw4KiO$DVVZdq2PL@80{|`}>{q z6h_j*ogEiA!eB6G$|iCY3}!>HymM?Jg;tdp4!N92YV=0P|Leo!@gSYcSK(P%6D)7sivdO-MMEj@U#qi9=$VXli;kTypq zBrutK(lfHY}dWRnwUzt48hPFY8X%zB?=$v~md(YXluYe)? zjCQwCbF^>Ws_p5>h>5^`lS%OdcTXPw^|<`y?UFw}20zk!tC($kgu%#1IH3rD3O;L@ z4i;v%A!jWgXJI}fV%9oIEewTOE$}{3S6_entCm1XOtK~1>q@&A8>zLI^bNaXhM1;ScW?`uTsWatLG7H*(v9rmc+Fbpun>eqf|r1_XfCJHi)-x z%e+>sNH)8LX+vWVdT(C|lQ8oq=VYu0Ok3F8*dMo5p}?{9`Xl{*m0#q;4cfy+QC*_$ zTpS>ynwTX?(Ng&?&$p%){#L33e-qSnlWt z1m4YRZ#GCzFi+tV7Kr#JMkmkMys*vOFKKGc8(3~1>jR`Sa#rsMxLC~|@1)#v)J5Qq znWI|d3a{962$tk=$&4^Dg&U@dX&N(5(YaJr2qxqHo0wiFU6jL-UB8cY9ct$+YJAyB zchJQb*+3LICU<$I9}nkRUE$PzJ;~?NCYUzL^;%Zbsm* z6E%GpPXA@2XRUZ>63iR{N^}%a@&k=r=)Sis+CO`Fe}}M%7O-rH8+0G8UaAyl_QY>} zKA<|I6tlI<1cS*a5ByJ@Qd2i49OHpkbC8N!!4yEbDCRckm&8-CeD?!o^MY`3q{HF3 zCp*v9(dcqahuj$@0q7(zG_RD&G31%Y2 zBksbj0=MQ#9Yp{ePM{_}>8>$gv-BsgjK1)TgC-RoU@EEnOT#$^`N0BqrK(WUOVs#l zr9|z9+^d{Y8Eu#Bl{x4kQL};ZwlDZ1=TU{dE8?(V0m+53B$EDk$vtmMp74i|EzG@r zwl{6R3$;^%@cG=9KotyKz+X%cHXGL<2ve&Az&E&cp9<8Z2R1AvZDg2GK|?BP#A`tw z(c#)X)}{_*1mP+zQOzY%z@24ST_p`^i1aGt)QCHL^;j2HBW*-IMJ(*3gW?=QE3`lv zu#1Y6;vDb>q>exfmiohxYW=$q9#j-6bisgC*Fm4PWS#oYtPcyj7CofxujonO-AMOx zW|}yqfB-7Dy$p*g-&lZ?0*Qwh);@^?lthdkt9E%85^5a-4@uBHSQ>yPQ8z;fiaYcW z3jbCu_)a}DdoZ)HmS!Qf3)-xuA@NZA7KD_P_mjNegzDLirwVEvUy* zXg)dQYfY*4W@$blt`4IA>HI>avw1=i4;@E|AI!o{M6)Iab#M=5uH_d_*+?VThA{bm E0q#rLzyJUM literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/text/writing-mode/tb-with-negative-dy-and-text-anchor.svg b/crates/resvg/tests/tests/text/writing-mode/tb-with-negative-dy-and-text-anchor.svg new file mode 100644 index 000000000..2fe69d582 --- /dev/null +++ b/crates/resvg/tests/tests/text/writing-mode/tb-with-negative-dy-and-text-anchor.svg @@ -0,0 +1,13 @@ + + `tb` with negative `dy` and text anchoring + + + + ABC + ABC + ABC + + + + diff --git a/crates/usvg/src/text/layout.rs b/crates/usvg/src/text/layout.rs index fca855467..7370a6a91 100644 --- a/crates/usvg/src/text/layout.rs +++ b/crates/usvg/src/text/layout.rs @@ -180,6 +180,7 @@ struct GlyphCluster { codepoint: char, width: f32, advance: f32, + text_length_spacing: f32, ascent: f32, descent: f32, has_relative_shift: bool, @@ -545,42 +546,70 @@ fn clusters_length(clusters: &[GlyphCluster]) -> f32 { clusters.iter().fold(0.0, |w, cluster| w + cluster.advance) } -fn resolve_clusters_positions_horizontal( +fn resolve_relative_shifts( text: &Text, chunk: &TextChunk, offset: usize, - writing_mode: WritingMode, - clusters: &mut [GlyphCluster], -) -> (f32, f32) { + clusters: &[GlyphCluster], +) -> (Vec<(f32, f32)>, Vec) { // `dx` and `dy` shift a character and all the characters after it in logical order, // while the clusters are in visual order, which differs in right-to-left text. let mut logical_order: Vec = (0..clusters.len()).collect(); logical_order.sort_by_key(|&i| clusters[i].byte_idx.value()); + let mut shifts = vec![(0.0, 0.0); clusters.len()]; let (mut shift_x, mut shift_y) = (0.0, 0.0); - for i in logical_order { + for &i in &logical_order { let cp = offset + clusters[i].byte_idx.code_point_at(&chunk.text); - if let (Some(dx), Some(dy)) = (text.dx.get(cp), text.dy.get(cp)) { - if writing_mode == WritingMode::LeftToRight { - shift_x += dx; - shift_y += dy; - } else { - shift_y -= dx; - shift_x += dy; - } - clusters[i].has_relative_shift = !dx.approx_zero_ulps(4) || !dy.approx_zero_ulps(4); - } + shift_x += text.dx.get(cp).cloned().unwrap_or(0.0); + shift_y += text.dy.get(cp).cloned().unwrap_or(0.0); shifts[i] = (shift_x, shift_y); } - let mut x = process_anchor(chunk.anchor, clusters_length(clusters)); + (shifts, logical_order) +} + +fn resolve_clusters_positions_horizontal( + text: &Text, + chunk: &TextChunk, + offset: usize, + writing_mode: WritingMode, + clusters: &mut [GlyphCluster], +) -> (f32, f32) { + let (mut shifts, logical_order) = resolve_relative_shifts(text, chunk, offset, clusters); + let first = logical_order[0]; + let last = logical_order[logical_order.len() - 1]; + + // Vertical text is laid out horizontally and rotated by 90 degrees afterwards. + if writing_mode == WritingMode::TopToBottom { + for shift in &mut shifts { + *shift = (shift.1, -shift.0); + } + } + + for i in 1..clusters.len() { + if shifts[i] != shifts[i - 1] { + clusters[i].has_relative_shift = true; + } + } - for (cluster, (cluster_shift_x, cluster_shift_y)) in clusters.iter_mut().zip(shifts) { + let mut left = f32::INFINITY; + let mut right = f32::NEG_INFINITY; + let mut advance = 0.0; + for (cluster, (shift_x, _)) in clusters.iter().zip(&shifts) { + let start = advance + shift_x; + let end = start + cluster.advance - cluster.text_length_spacing; + left = left.min(start).min(end); + right = right.max(start).max(end); + advance += cluster.advance; + } + + let mut x = shifts[first].0 - left + process_anchor(chunk.anchor, right - left); + + for (cluster, (shift_x, shift_y)) in clusters.iter_mut().zip(&shifts) { let cp = offset + cluster.byte_idx.code_point_at(&chunk.text); - cluster.transform = cluster - .transform - .pre_translate(x + cluster_shift_x, cluster_shift_y); + cluster.transform = cluster.transform.pre_translate(x + shift_x, *shift_y); if let Some(angle) = text.rotate.get(cp).cloned() { if !angle.approx_zero_ulps(4) { @@ -592,7 +621,7 @@ fn resolve_clusters_positions_horizontal( x += cluster.advance; } - (x + shift_x, shift_y) + (x + shifts[last].0, shifts[last].1) } // Baseline resolving in SVG is a mess. @@ -650,7 +679,7 @@ fn resolve_clusters_positions_path( let mut last_x = 0.0; let mut last_y = 0.0; - let mut dy = 0.0; + let (shifts, _) = resolve_relative_shifts(text, chunk, char_offset, clusters); // In the text path mode, chunk's x/y coordinates provide an additional offset along the path. // The X coordinate is used in a horizontal mode, and Y in vertical. @@ -662,8 +691,8 @@ fn resolve_clusters_positions_path( let start_offset = chunk_offset + path.start_offset + process_anchor(chunk.anchor, clusters_length(clusters)); - let normals = collect_normals(text, chunk, clusters, &path.path, char_offset, start_offset); - for (cluster, normal) in clusters.iter_mut().zip(normals) { + let normals = collect_normals(text, clusters, &shifts, &path.path, start_offset); + for ((cluster, normal), (_, dy)) in clusters.iter_mut().zip(normals).zip(shifts) { let (x, y, angle) = match normal { Some(normal) => (normal.x, normal.y, normal.angle), None => { @@ -685,7 +714,6 @@ fn resolve_clusters_positions_path( cluster.transform = cluster.transform.pre_rotate_at(angle, half_width, 0.0); let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text); - dy += text.dy.get(cp).cloned().unwrap_or(0.0); let baseline_shift = chunk_span_at(chunk, cluster.byte_idx) .map(|span| { @@ -738,25 +766,20 @@ pub(crate) struct PathNormal { fn collect_normals( text: &Text, - chunk: &TextChunk, clusters: &[GlyphCluster], + shifts: &[(f32, f32)], path: &tiny_skia_path::Path, - char_offset: usize, offset: f32, ) -> Vec> { let mut offsets = Vec::with_capacity(clusters.len()); let mut normals = Vec::with_capacity(clusters.len()); { let mut advance = offset; - for cluster in clusters { + for (cluster, (shift_x, _)) in clusters.iter().zip(shifts) { // Clusters should be rotated by the x-midpoint x baseline position. let half_width = cluster.width / 2.0; - // Include relative position. - let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text); - advance += text.dx.get(cp).cloned().unwrap_or(0.0); - - let offset = advance + half_width; + let offset = advance + shift_x + half_width; // Clusters outside the path have no normals. if offset < 0.0 { @@ -1032,6 +1055,7 @@ fn apply_length_adjust(chunk: &TextChunk, clusters: &mut [GlyphCluster]) { for i in cluster_indexes { clusters[i].advance = clusters[i].width + factor; + clusters[i].text_length_spacing = factor; } } else { let factor = target_width / width; @@ -1205,6 +1229,7 @@ fn form_glyph_clusters(glyphs: &[Glyph], text: &str, font_size: f32) -> GlyphClu codepoint: byte_idx.char_from(text), width, advance, + text_length_spacing: 0.0, ascent: font.ascent(font_size), descent: font.descent(font_size), has_relative_shift: false,