diff --git a/README.md b/README.md
index 1340126a..b96a313a 100644
--- a/README.md
+++ b/README.md
@@ -270,6 +270,18 @@ Satori uses the same Flexbox [layout engine](https://yogalayout.com) as React Na
maskSize | Support two-value size i.e. `10px 20%` | Example |
maskRepeat | repeat , repeat-x , repeat-y , no-repeat , defaults to repeat | Example |
+
+WebkitTextStroke
+ | WebkitTextStrokeWidth |
+Supported |
+ |
+
+
+WebkitTextStrokeColor |
+Supported |
+ |
+
+
diff --git a/src/builder/text.ts b/src/builder/text.ts
index 26eddbae..6ab354b8 100644
--- a/src/builder/text.ts
+++ b/src/builder/text.ts
@@ -132,6 +132,14 @@ export default function buildText(
transform: matrix || undefined,
'clip-path': clipPathId ? `url(#${clipPathId})` : undefined,
style: style.filter ? `filter:${style.filter}` : undefined,
+ 'stroke-width': style.WebkitTextStrokeWidth
+ ? `${style.WebkitTextStrokeWidth}px`
+ : undefined,
+ stroke: style.WebkitTextStrokeWidth
+ ? style.WebkitTextStrokeColor
+ : undefined,
+ 'stroke-linejoin': style.WebkitTextStrokeWidth ? 'round' : undefined,
+ 'paint-order': style.WebkitTextStrokeWidth ? 'stroke' : undefined,
}
return [
(filter ? `${filter}` : '') +
diff --git a/src/handler/expand.ts b/src/handler/expand.ts
index 92fbc775..a1f39ba0 100644
--- a/src/handler/expand.ts
+++ b/src/handler/expand.ts
@@ -195,6 +195,19 @@ function handleSpecialCase(
return result
}
+ if (name === 'WebkitTextStroke') {
+ value = value.toString().trim()
+ const values = value.split(' ')
+ if (values.length !== 2) {
+ throw new Error('Invalid `WebkitTextStroke` value.')
+ }
+
+ return {
+ WebkitTextStrokeWidth: purify(name, values[0]),
+ WebkitTextStrokeColor: purify(name, values[1]),
+ }
+ }
+
return
}
@@ -267,6 +280,8 @@ type MainStyle = {
}[]
textShadowColor: string[]
textShadowRadius: number[]
+ WebkitTextStrokeWidth: number
+ WebkitTextStrokeColor: string
}
type OtherStyle = Exclude, keyof MainStyle>
diff --git a/src/handler/inheritable.ts b/src/handler/inheritable.ts
index 07996e72..fefa84ae 100644
--- a/src/handler/inheritable.ts
+++ b/src/handler/inheritable.ts
@@ -14,6 +14,8 @@ const list = new Set([
'textShadowOffset',
'textShadowColor',
'textShadowRadius',
+ 'WebkitTextStrokeWidth',
+ 'WebkitTextStrokeColor',
'textDecorationLine',
'textDecorationStyle',
'textDecorationColor',
diff --git a/src/text/index.ts b/src/text/index.ts
index 7149a84b..d40242bb 100644
--- a/src/text/index.ts
+++ b/src/text/index.ts
@@ -734,6 +734,18 @@ export default async function* buildTextNodes(
mask: overflowMaskId ? `url(#${overflowMaskId})` : undefined,
style: cssFilter ? `filter:${cssFilter}` : undefined,
+ 'stroke-width': inheritedStyle.WebkitTextStrokeWidth
+ ? `${inheritedStyle.WebkitTextStrokeWidth}px`
+ : undefined,
+ stroke: inheritedStyle.WebkitTextStrokeWidth
+ ? inheritedStyle.WebkitTextStrokeColor
+ : undefined,
+ 'stroke-linejoin': inheritedStyle.WebkitTextStrokeWidth
+ ? 'round'
+ : undefined,
+ 'paint-order': inheritedStyle.WebkitTextStrokeWidth
+ ? 'stroke'
+ : undefined,
})
: ''
diff --git a/test/__image_snapshots__/webkit-text-stroke-test-tsx-test-webkit-text-stroke-test-tsx-webkit-text-stroke-should-work-basic-text-stroke-1-snap.png b/test/__image_snapshots__/webkit-text-stroke-test-tsx-test-webkit-text-stroke-test-tsx-webkit-text-stroke-should-work-basic-text-stroke-1-snap.png
new file mode 100644
index 00000000..40a57551
Binary files /dev/null and b/test/__image_snapshots__/webkit-text-stroke-test-tsx-test-webkit-text-stroke-test-tsx-webkit-text-stroke-should-work-basic-text-stroke-1-snap.png differ
diff --git a/test/__image_snapshots__/webkit-text-stroke-test-tsx-test-webkit-text-stroke-test-tsx-webkit-text-stroke-should-work-nested-and-complex-text-stroke-1-snap.png b/test/__image_snapshots__/webkit-text-stroke-test-tsx-test-webkit-text-stroke-test-tsx-webkit-text-stroke-should-work-nested-and-complex-text-stroke-1-snap.png
new file mode 100644
index 00000000..0b3dc04c
Binary files /dev/null and b/test/__image_snapshots__/webkit-text-stroke-test-tsx-test-webkit-text-stroke-test-tsx-webkit-text-stroke-should-work-nested-and-complex-text-stroke-1-snap.png differ
diff --git a/test/__image_snapshots__/webkit-text-stroke-test-tsx-test-webkit-text-stroke-test-tsx-webkit-text-stroke-should-work-nested-text-stroke-1-snap.png b/test/__image_snapshots__/webkit-text-stroke-test-tsx-test-webkit-text-stroke-test-tsx-webkit-text-stroke-should-work-nested-text-stroke-1-snap.png
new file mode 100644
index 00000000..8a4fa0fc
Binary files /dev/null and b/test/__image_snapshots__/webkit-text-stroke-test-tsx-test-webkit-text-stroke-test-tsx-webkit-text-stroke-should-work-nested-text-stroke-1-snap.png differ
diff --git a/test/webkit-text-stroke.test.tsx b/test/webkit-text-stroke.test.tsx
new file mode 100644
index 00000000..d0ec457a
--- /dev/null
+++ b/test/webkit-text-stroke.test.tsx
@@ -0,0 +1,83 @@
+import { it, describe, expect } from 'vitest'
+
+import { initFonts, toImage } from './utils.js'
+import satori from '../src/index.js'
+
+describe('webkit-text-stroke', () => {
+ let fonts
+ initFonts((f) => (fonts = f))
+
+ it('should work basic text stroke', async () => {
+ const svg = await satori(
+
+ Hello, world
+
,
+ { width: 100, height: 100, fonts }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should work nested text stroke', async () => {
+ const svg = await satori(
+
+ Hello, world
+
,
+ { width: 100, height: 100, fonts }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should work nested and complex text stroke', async () => {
+ const svg = await satori(
+
+ Hello,
+ w
+ o
+ r
+ l
+ d
+
+ !
+
+
,
+ { width: 100, height: 100, fonts }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+})