From 3b4a0ffefcb37862ac70bd2a8eb9b2ed567e1f85 Mon Sep 17 00:00:00 2001 From: Dany Date: Thu, 15 Oct 2026 09:10:15 -0700 Subject: [PATCH] fix: respect preserveAspectRatio meet alignment in SVG use --- src/shape/svg/svg_import.js | 15 ++- ...defs-symbol-preserve-aspect-ratio-meet.svg | 27 ++++++ test/unit/svg/svg_import.js | 89 ++++++++++++++++++ test/unit/visual/cases/svg.js | 10 ++ .../000.png | Bin 0 -> 4022 bytes .../metadata.json | 3 + 6 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 test/unit/assets/svg/defs-symbol-preserve-aspect-ratio-meet.svg create mode 100644 test/unit/visual/screenshots/svg/SVG Import - defs and use/symbol preserveAspectRatio meet alignments/000.png create mode 100644 test/unit/visual/screenshots/svg/SVG Import - defs and use/symbol preserveAspectRatio meet alignments/metadata.json diff --git a/src/shape/svg/svg_import.js b/src/shape/svg/svg_import.js index b0ed21c138..f82eb09052 100644 --- a/src/shape/svg/svg_import.js +++ b/src/shape/svg/svg_import.js @@ -711,11 +711,20 @@ export function SVGImportAddon(p5, fn, lifecycles) { ); this.tStack.current.scaleSelf(scaleX, scaleY); } else { - const scale = Math.min(w / vb.width, h / vb.height); // default: xMidYMid meet + const scale = Math.min(w / vb.width, h / vb.height); + + // Default SVG alignment is xMidYMid meet. + const alignment = preserveAspectRatio?.trim().split(/\s+/)[0] || "xMidYMid"; + + const alignX = alignment.startsWith("xMin") ? 0 + : alignment.startsWith("xMax") ? 1 : 0.5; + + const alignY = alignment.endsWith("YMin") ? 0 + : alignment.endsWith("YMax") ? 1 : 0.5; this.tStack.current.translateSelf( - (w - vb.width * scale) / 2 - vb.x * scale, - (h - vb.height * scale) / 2 - vb.y * scale + (w - vb.width * scale) * alignX - vb.x * scale, + (h - vb.height * scale) * alignY - vb.y * scale ); this.tStack.current.scaleSelf(scale, scale); } diff --git a/test/unit/assets/svg/defs-symbol-preserve-aspect-ratio-meet.svg b/test/unit/assets/svg/defs-symbol-preserve-aspect-ratio-meet.svg new file mode 100644 index 0000000000..1fcc6fb932 --- /dev/null +++ b/test/unit/assets/svg/defs-symbol-preserve-aspect-ratio-meet.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/unit/svg/svg_import.js b/test/unit/svg/svg_import.js index d4acf1d594..59cf8810a3 100644 --- a/test/unit/svg/svg_import.js +++ b/test/unit/svg/svg_import.js @@ -1151,6 +1151,95 @@ suite(' and elements', function () { assert.closeTo(node.state.transform.d, 1, 0.001); }); + test('use respects preserveAspectRatio=xMinYMin meet', function () { + const record = createSVG(` + + + + + + + + + `); + + const scope = firstChild(record); + const node = scope.children[0]; + + assert.closeTo(node.state.transform.a, 1, 0.001); + assert.closeTo(node.state.transform.d, 1, 0.001); + assert.closeTo(node.state.transform.e, 0, 0.001); + assert.closeTo(node.state.transform.f, 0, 0.001); + }); + + const meetAlignments = [ + ['xMinYMin', 0, 0], + ['xMidYMin', 0.5, 0], + ['xMaxYMin', 1, 0], + ['xMinYMid', 0, 0.5], + ['xMidYMid', 0.5, 0.5], + ['xMaxYMid', 1, 0.5], + ['xMinYMax', 0, 1], + ['xMidYMax', 0.5, 1], + ['xMaxYMax', 1, 1] + ]; + + for (const [alignment, alignX, alignY] of meetAlignments) { + for (const [width, height] of [[200, 100], [100, 200]]) { + test(`use respects ${alignment} meet in ${width}x${height}`, function () { + const record = createSVG(` + + + + + + + + + `); + + const node = firstChild(record).children[0]; + const scale = Math.min(width / 100, height / 100); + + assert.closeTo(node.state.transform.a, scale, 0.001); + assert.closeTo(node.state.transform.d, scale, 0.001); + assert.closeTo( + node.state.transform.e, + (width - 100 * scale) * alignX, + 0.001 + ); + assert.closeTo( + node.state.transform.f, + (height - 100 * scale) * alignY, + 0.001 + ); + }); + } + } + + test('use meet alignment accounts for viewBox origin', function () { + const record = createSVG(` + + + + + + + + + `); + + const node = firstChild(record).children[0]; + + assert.closeTo(node.state.transform.a, 1, 0.001); + assert.closeTo(node.state.transform.d, 1, 0.001); + assert.closeTo(node.state.transform.e, 90, 0.001); + assert.closeTo(node.state.transform.f, -20, 0.001); + }); + test('use with preserveAspectRatio=none accounts for viewBox origin', function () { const record = createSVG(` diff --git a/test/unit/visual/cases/svg.js b/test/unit/visual/cases/svg.js index 4a8d615a6b..754da3fac9 100644 --- a/test/unit/visual/cases/svg.js +++ b/test/unit/visual/cases/svg.js @@ -723,6 +723,16 @@ visualSuite('svg', function () { await screenshot(); }); + visualTest('symbol preserveAspectRatio meet alignments', async (p, screenshot) => { + setupDefault(p); + const shape = await loadFixture( + p, + 'defs-symbol-preserve-aspect-ratio-meet' + ); + p.shape(shape); + await screenshot(); + }); + visualTest('x and y', async (p, screenshot) => { setupDefault(p); const shape = await loadFixture(p, 'defs-use-xy'); diff --git a/test/unit/visual/screenshots/svg/SVG Import - defs and use/symbol preserveAspectRatio meet alignments/000.png b/test/unit/visual/screenshots/svg/SVG Import - defs and use/symbol preserveAspectRatio meet alignments/000.png new file mode 100644 index 0000000000000000000000000000000000000000..4e233500a2d0c96ceb88e1cde799c39a4515339c GIT binary patch literal 4022 zcmaKvdpy(q`^RUS*|aihGa@6$saPab%#cmxG;&C24jstX>6UWZoT5o;l5$+hl4x|7 zBm(?bdQ=rbAacPp8!OissV{{?;p3(XDXjapxb|cd3XZy|oq1A;TdSlK zz#4uh|1-SSw}O-$Dz7<9hv#>)OOQ}E&s6w5`vP|Fa-@0TGH;*Cq$)l^W;y& zAvCi!cX(l7-890PH6j5LrqGTw^HPANLFMZh@0XRb$aM1|!3~Wz;w!V3e*JhB zzLr<+N$Nezazmzchwt7rG~3J$S0jRur2s!HlpDFUC~nST&-NH!$#>QyVT^hY6Qa=O zcGPpE$WhNzq+Skd8p`$WHG`ruudeBN#AX$tqp--rq^$6LkjOhj-BX_EEnQ@HR;Ay$J#5xZrAcj!IlNHg{*gXDt#V|v!KKL>@d%Z&&WcA3Z&NlY|D%h|(!x(_ zV9a5vj1$Rk{dNWN{vi3ak`-CKN|P9RehXqiZ&xK-N*{q!M5?m74NqCW9W#346U`{m zg0a`6WRZ{N)uTdYuLd_e?)v)f9LrXkqEoVYJQSf87x_ED0C7Ue!<7&PHK&BWSa_d5 zr4vhXdM02%xv*Z8{rWb=Oqax;87u_^Zaq?!>ELup(_$e>=y3(_0W!fUA9=7>p|WWH ziSY4ap`iPKqCE0)@j9nxJMez5$J%|-oq+;xf@>UcWiX}eivSw=)M1KIA`SZl<&M#R z9@1Qc@bKH-r-itqWQavxE;=`IX1!n1kg|IW<9)6ao#phoe4*AVxL63U$Js5%D?L0r z83g6F&_7pFA^G@&3oTffp`SK}xchPu0jKy@#24Vo@JMtXGUcg5s+`rGX%Qh8+Z5Ky z%pl+T;u;ftCc|pWlxfR|0jx`!V#}*Bu+^(huXEkqTDJyqO{*Vt8byq~ht^@H+jgRi zwz63oh-)8H@vokY{YUBmX9|c(=ajne(vtWfq?fsvg{n~V_A~oLz6$$Wg?Et68VRXdTj`< zp~M(HPH}PZbeO5BxG^?1RzLN5qHk%QnOPbB77mGBjM-f_V>cn6JU`sn)bxnQ)0zMK z@3E$)rrRl$ZR5tu9&Co6HtaRDJ(_uhCJVI#k3m+@|fmQ{UuwB#g{*7+J`LUZ5AJx8Pyy3UXaA(G1Eq@nM9L8}Uj%`8M<8?u^*Az{# zIVZ%wN5t&v(2FhU6Uk1W5_s7g&D!wFY?e3ToHQl1%;kxcb?wY>{eYPLwkjyo=^@2^ zHsRq75Y;ETbQiCyfV_NFR&{bsOWtC<{L%@rHH8}~?H!@{X68;kzCEynN^lJ#Mq&1T zp0^FH(OdM;O>bEgS5dOYul@b90iVYp93pTvVn?PI8N?2ysiPc1&cd$L)NCXF!wkY% zRtU@K;!mEZ#iKg>1l+p?#|@h;O!8Ho^&Uixbj!ZlgGbP+j+m#t*FK2^8({s= ztI$`E@NX3Wf^r?`vB$fa@a?p6^js$sUNiCX&9DxDk+$9?VG*!%alhUzYJx~j+wE_S zU<%Zpm+oI)8xdG{meX_m90ApezUa^W1tJe7+Sv1TK!oEQHueW#RE6dB8u-W8X;P@t zwHGM;x**A3xq|~I9y8%D((Je)IZ~*A8!{Cq?;jyUk3arU4_&mq(&KPib!am5=c7R^WcjyPy# zV+&BK$=&iUM>@S@`Wn?hgO<}h7e8-^%KT>iJ=v`9y58Ddh6Gj=Vb3}muf$&znR(U! ze05a<7{Tv{r=pPNwnS+O&w_`Gq)?)w4*_rq;{pY%JUeRWbd1Q_px{2W_h^ar{!zc= zuQ`Q%C-K?jkeEW7n?}9rW$0>QG$4fu$Y;`!b^V3>B>KkQ=Nb&%^QN z&_ea1jT`;hJ$dK*LRLRNaQy{p5=l!IK`j=vb9bB+2~=44)M1@1h0Lt%`q;Rct{qeX* zOscHo&ASMD2=hRuq`o2lFw2<0!zD@Ft+b!X2Qpth`ZNXh(_dyPHYGLmQ5nUJ4hqR0 zYt^N(UPcyvFiboMj7qkQuG5uMR=+83I& zQUE9v@O($;erevN3y1nE74lWSXnc|LgdUIi4&Jz9xmz?CQFMCGNn+W`$$gM#IA7xc4NajE_y${H~rtuRhMn2bCV72n+=vB2iGyU1X*!&y{y@#V6COlA+7AB z8*Eq}}aIR=X51&?`G+;vdexSbIrkJC@i&|| zO-I3+cV4IMjSVD2`=>gBNA7`R67*d2M&ouT?d-;yoAKf-Q0bIi)!GR#i{pH)fg$k8 zm+AKVqbeY+%7cL5Zb+hc(zD&Yosf$4?VSyQ51bJT9~8WU`-|W;xg4iWS>3{s?_-Px zguQ0=j)J5JQzHOO2_G#SqnFiLQ5KNYl^|x8)x<(VL>3Txb#D$sKeslF`H)tzNo(s& zo$#68gDO>ga&DIpB2j$7Z7RXF-}YJTBkZfG^hidpYCd*nb@Z<0@q6$Z&4F`2lBEC< z$DXtVChZ8>c7Z*jncb@AF36L3HHz3z?i^X)v(+q@oEvbcx(Z(cm{_GPvxzT^_UnkV zhaRU?lkHc%WpNTxb~6q`tNq;aIOtwLN=}+0EM&HB_qJ#b8F#~r#Xw9VqtaqRLo&Bo z_?zH@71E_ET2um=4rDw04rLFK%m_>Co*qh+=_SR(m#puVcbO_b9XE5J@=)hK0`ATO z(I6ftQGSoM#tC(Q^!(tEBa}GtSeOKrddMcfoL>{aXoR}Vj;1^*qNQpH{{0vd;c><8 zWj>r?e^=m-xV03;jHwDhBRsU4>MRtf+x%+`7|e5y9}OZlg+bSJvTl~q6{vJ`@tTm1 zkn#oB<%1+4^d8nn)(%P|-S`=VmdL;^hymFzDgLx^9gU#t=h)84W5!h}~Q(1y|n zWvy33wb4$a<=+no5M+UNo4|#mhbl##2XDx!ycfK_;0dAj-mMN#eOF_Z5#3F$(Zr>n z!WkJ-dZ0XIbqbFnfKXicmOLuQz&z7Pfir}+4Of;fi*DZH|D~bO?xOSXo+%KCi!KA& zIMSgf9I}@Yv7xozu4M2!n@V6E9?B;v#XsAo!5XZom|!4o zNtnp|f0)Q@cZO!P_woy*62z8(iI9K8#HaSh1RhW&f-U_gdX z_j8B@lW4)W6inNonEl>8R!u>+Q2+g<&2?!L1@)_$?&3@S2Z>c={Z0QsBI-}LOUt#m z?v=Y{#xX!HLK|@I82hC{XedqbBH7Qu-30c(JnSd5twW}=#T!JF?mBg?Z11*!#31IC z(8?B5otST%kspK3{5`u4zP(OHDP774TuO~jNH<6W3~epAKIhlQgfsS5GHV5ZZK?EE zn>z7tYssC*urmh%(;h@zdUqBDuC1s_=#VU+=|KmVMMEI+NL;s)LlSFF6NV