From 5928cf4b2876005bc1fd8d98e3b782a5dbe9129b Mon Sep 17 00:00:00 2001 From: Dany Date: Wed, 7 Oct 2026 08:07:51 -0700 Subject: [PATCH 1/2] fix SVG preserveAspectRatio none handling --- src/shape/svg/svg_import.js | 28 +++++++++++++++++++++------- test/unit/svg/svg_import.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/shape/svg/svg_import.js b/src/shape/svg/svg_import.js index 5f363c8b54..b0ed21c138 100644 --- a/src/shape/svg/svg_import.js +++ b/src/shape/svg/svg_import.js @@ -699,12 +699,26 @@ export function SVGImportAddon(p5, fn, lifecycles) { ? this.num(node, "height") : (referenced.height?.baseVal?.value || vb.height); - const scale = Math.min(w / vb.width, h / vb.height); // default: xMidYMid meet - this.tStack.current.translateSelf( - (w - vb.width * scale) / 2 - vb.x * scale, - (h - vb.height * scale) / 2 - vb.y * scale - ); - this.tStack.current.scaleSelf(scale, scale); + const preserveAspectRatio = referenced.getAttribute("preserveAspectRatio"); + + if (preserveAspectRatio?.trim() === "none") { + const scaleX = w / vb.width; + const scaleY = h / vb.height; + + this.tStack.current.translateSelf( + -vb.x * scaleX, + -vb.y * scaleY + ); + this.tStack.current.scaleSelf(scaleX, scaleY); + } else { + const scale = Math.min(w / vb.width, h / vb.height); // default: xMidYMid meet + + this.tStack.current.translateSelf( + (w - vb.width * scale) / 2 - vb.x * scale, + (h - vb.height * scale) / 2 - vb.y * scale + ); + this.tStack.current.scaleSelf(scale, scale); + } } this.visit(referenced); }); @@ -1623,4 +1637,4 @@ export function SVGImportAddon(p5, fn, lifecycles) { if (typeof p5 !== 'undefined') { p5.registerAddon(SVGImportAddon); -} \ No newline at end of file +} diff --git a/test/unit/svg/svg_import.js b/test/unit/svg/svg_import.js index a0545b9e5e..d4acf1d594 100644 --- a/test/unit/svg/svg_import.js +++ b/test/unit/svg/svg_import.js @@ -1134,6 +1134,43 @@ suite(' and elements', function () { }); }); + test('use respects preserveAspectRatio=none', function () { + const record = createSVG(` + + + + + + + + + `); + const scope = firstChild(record); + const node = scope.children[0]; + assert.closeTo(node.state.transform.a, 2, 0.001); + assert.closeTo(node.state.transform.d, 1, 0.001); + }); + + test('use with preserveAspectRatio=none accounts for viewBox origin', function () { + const record = createSVG(` + + + + + + + + + `); + const scope = firstChild(record); + const node = scope.children[0]; + + assert.closeTo(node.state.transform.a, 2, 0.001); + assert.closeTo(node.state.transform.d, 1, 0.001); + assert.closeTo(node.state.transform.e, -20, 0.001); + assert.closeTo(node.state.transform.f, -20, 0.001); + }); + test('handles missing references gracefully', function () { const record = createSVG(` From a5ec8398230a29609a9df970e73298ceefa4fdd0 Mon Sep 17 00:00:00 2001 From: Dany Date: Fri, 9 Oct 2026 08:01:56 -0700 Subject: [PATCH 2/2] test: add visual regression coverage for SVG preserveAspectRatio none --- ...defs-symbol-preserve-aspect-ratio-none.svg | 19 ++++++++++++++++++ test/unit/visual/cases/svg.js | 10 +++++++++ .../symbol preserveAspectRatio none/000.png | Bin 0 -> 5272 bytes .../metadata.json | 3 +++ 4 files changed, 32 insertions(+) create mode 100644 test/unit/assets/svg/defs-symbol-preserve-aspect-ratio-none.svg create mode 100644 test/unit/visual/screenshots/svg/SVG Import - defs and use/symbol preserveAspectRatio none/000.png create mode 100644 test/unit/visual/screenshots/svg/SVG Import - defs and use/symbol preserveAspectRatio none/metadata.json diff --git a/test/unit/assets/svg/defs-symbol-preserve-aspect-ratio-none.svg b/test/unit/assets/svg/defs-symbol-preserve-aspect-ratio-none.svg new file mode 100644 index 0000000000..363babf96a --- /dev/null +++ b/test/unit/assets/svg/defs-symbol-preserve-aspect-ratio-none.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + diff --git a/test/unit/visual/cases/svg.js b/test/unit/visual/cases/svg.js index 82312cef9a..4a8d615a6b 100644 --- a/test/unit/visual/cases/svg.js +++ b/test/unit/visual/cases/svg.js @@ -713,6 +713,16 @@ visualSuite('svg', function () { await screenshot(); }); + visualTest('symbol preserveAspectRatio none', async (p, screenshot) => { + setupDefault(p); + const shape = await loadFixture( + p, + 'defs-symbol-preserve-aspect-ratio-none' + ); + 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 none/000.png b/test/unit/visual/screenshots/svg/SVG Import - defs and use/symbol preserveAspectRatio none/000.png new file mode 100644 index 0000000000000000000000000000000000000000..7cb512e0ada6bc1e0ae9f2e5c910dcd8583b217c GIT binary patch literal 5272 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/svg/SVG Import - defs and use/symbol preserveAspectRatio none/metadata.json b/test/unit/visual/screenshots/svg/SVG Import - defs and use/symbol preserveAspectRatio none/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/svg/SVG Import - defs and use/symbol preserveAspectRatio none/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file