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 maskSizeSupport two-value size i.e. `10px 20%`Example maskRepeatrepeat, repeat-x, repeat-y, no-repeat, defaults to repeatExample + +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() + }) +})