From c11d3aaaa74d42e7c9a4a4a87c35445f5e58982c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Torr=C3=B3?= Date: Tue, 14 Jul 2026 10:26:19 +0200 Subject: [PATCH] :bug: Add :stroke-image support to plugins API (#10683) --- frontend/src/app/plugins/format.cljs | 7 ++-- frontend/src/app/plugins/parser.cljs | 4 ++- frontend/src/app/plugins/strokes.cljs | 10 ++++-- .../src/generated/api-surface.json | 7 ++++ .../src/tests/media.test.ts | 33 +++++++++++++++++++ plugins/libs/plugin-types/index.d.ts | 4 +++ 6 files changed, 60 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/plugins/format.cljs b/frontend/src/app/plugins/format.cljs index 1f0701dd99..8680a6d8a0 100644 --- a/frontend/src/app/plugins/format.cljs +++ b/frontend/src/app/plugins/format.cljs @@ -206,11 +206,13 @@ ;; strokeCapStart?: StrokeCap; ;; strokeCapEnd?: StrokeCap; ;; strokeColorGradient?: Gradient; +;; strokeImage?: ImageData; ;; } (defn format-stroke [{:keys [stroke-color stroke-color-ref-file stroke-color-ref-id stroke-opacity stroke-style stroke-width stroke-alignment - stroke-cap-start stroke-cap-end stroke-color-gradient] :as stroke}] + stroke-cap-start stroke-cap-end stroke-color-gradient + stroke-image] :as stroke}] (when (some? stroke) (obj/without-empty @@ -223,7 +225,8 @@ :strokeAlignment (format-key stroke-alignment) :strokeCapStart (format-key stroke-cap-start) :strokeCapEnd (format-key stroke-cap-end) - :strokeColorGradient (format-gradient stroke-color-gradient)}))) + :strokeColorGradient (format-gradient stroke-color-gradient) + :strokeImage (format-image stroke-image)}))) ;; export interface Blur { diff --git a/frontend/src/app/plugins/parser.cljs b/frontend/src/app/plugins/parser.cljs index 1b9f844c48..cbadff48f4 100644 --- a/frontend/src/app/plugins/parser.cljs +++ b/frontend/src/app/plugins/parser.cljs @@ -214,6 +214,7 @@ ;; strokeCapStart?: StrokeCap; ;; strokeCapEnd?: StrokeCap; ;; strokeColorGradient?: Gradient; +;; strokeImage?: ImageData; ;; } (defn parse-stroke [^js stroke] @@ -228,7 +229,8 @@ :stroke-alignment (-> (obj/get stroke "strokeAlignment") parse-keyword) :stroke-cap-start (-> (obj/get stroke "strokeCapStart") parse-keyword) :stroke-cap-end (-> (obj/get stroke "strokeCapEnd") parse-keyword) - :stroke-color-gradient (-> (obj/get stroke "strokeColorGradient") parse-gradient)}))) + :stroke-color-gradient (-> (obj/get stroke "strokeColorGradient") parse-gradient) + :stroke-image (-> (obj/get stroke "strokeImage") parse-image-data)}))) (defn parse-strokes [^js strokes] diff --git a/frontend/src/app/plugins/strokes.cljs b/frontend/src/app/plugins/strokes.cljs index 59e305d4aa..1a717bb21c 100644 --- a/frontend/src/app/plugins/strokes.cljs +++ b/frontend/src/app/plugins/strokes.cljs @@ -18,7 +18,7 @@ :strokeColor {:get (fn [] (:stroke-color @state)) :set (fn [v] - (swap! state #(-> % (assoc :stroke-color v) (dissoc :stroke-color-gradient))) + (swap! state #(-> % (assoc :stroke-color v) (dissoc :stroke-color-gradient :stroke-image))) (on-change!))} :strokeColorRefFile @@ -62,7 +62,13 @@ (on-change!))] (gradients/gradient-proxy gradient-state gradient-change!)))) :set (fn [v] - (swap! state #(-> % (assoc :stroke-color-gradient (parser/parse-gradient v)) (dissoc :stroke-color))) + (swap! state #(-> % (assoc :stroke-color-gradient (parser/parse-gradient v)) (dissoc :stroke-color :stroke-image))) + (on-change!))} + + :strokeImage + {:get (fn [] (format/format-image (:stroke-image @state))) + :set (fn [v] + (swap! state #(-> % (assoc :stroke-image (parser/parse-image-data v)) (dissoc :stroke-color :stroke-color-gradient))) (on-change!))}))) (defn format-strokes diff --git a/plugins/apps/plugin-api-test-suite/src/generated/api-surface.json b/plugins/apps/plugin-api-test-suite/src/generated/api-surface.json index f61aac311a..0cbb2f8e1d 100644 --- a/plugins/apps/plugin-api-test-suite/src/generated/api-surface.json +++ b/plugins/apps/plugin-api-test-suite/src/generated/api-surface.json @@ -467,6 +467,7 @@ "strokeColorGradient", "strokeColorRefFile", "strokeColorRefId", + "strokeImage", "strokeOpacity", "strokeStyle", "strokeWidth" @@ -7791,6 +7792,12 @@ "kind": "getset", "type": "Gradient", "array": false + }, + "strokeImage": { + "decl": "Stroke", + "kind": "getset", + "type": "ImageData", + "array": false } }, "SvgRaw": { diff --git a/plugins/apps/plugin-api-test-suite/src/tests/media.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/media.test.ts index c05ba336e2..3f4b4b01bb 100644 --- a/plugins/apps/plugin-api-test-suite/src/tests/media.test.ts +++ b/plugins/apps/plugin-api-test-suite/src/tests/media.test.ts @@ -61,6 +61,39 @@ describe.skipIfMocked('Media', () => { expect(fill.fillImage).toBeDefined(); }); + test('an uploaded image can be used as a stroke', async (ctx) => { + const image = await ctx.penpot.uploadMediaData( + 'plugin-stroke', + PNG_1X1, + 'image/png', + ); + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + rect.strokes = [{ strokeWidth: 2, strokeImage: image }]; + + const strokes = rect.strokes; + expect(strokes).toHaveLength(1); + expect(strokes[0].strokeImage).toBeDefined(); + }); + + test('Stroke.strokeImage can be set on a stroke and clears the solid color', async (ctx) => { + const image = await ctx.penpot.uploadMediaData( + 'plugin-stroke-set', + PNG_1X1, + 'image/png', + ); + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + rect.strokes = [{ strokeColor: '#ff0000', strokeWidth: 2 }]; + + // Set strokeImage directly on the stroke (covers Stroke.strokeImage (set)). + const stroke = rect.strokes[0]; + stroke.strokeImage = image; + expect(stroke.strokeImage).toBeDefined(); + // An image stroke replaces the solid color. + expect(stroke.strokeColor).toBeFalsy(); + }); + test('uploadMediaUrl resolves to image data', async (ctx) => { // Needs the backend to fetch an external URL, which may be unavailable in // the headless runner; treat a rejection as an environment limitation. diff --git a/plugins/libs/plugin-types/index.d.ts b/plugins/libs/plugin-types/index.d.ts index 1dbe005a3e..cb53532dd6 100644 --- a/plugins/libs/plugin-types/index.d.ts +++ b/plugins/libs/plugin-types/index.d.ts @@ -4180,6 +4180,10 @@ export interface Stroke { * The optional gradient stroke defined by a Gradient object. */ strokeColorGradient?: Gradient; + /** + * The optional image stroke defined by an ImageData object. + */ + strokeImage?: ImageData; } /**