diff --git a/src/builder/background-image.ts b/src/builder/background-image.ts index 69b8fe0f..af11afb8 100644 --- a/src/builder/background-image.ts +++ b/src/builder/background-image.ts @@ -161,17 +161,6 @@ function normalizeStops( } } - if (from === 'mask') { - return stops.map((stop) => { - const color = cssColorParse(stop.color) - if (color.alpha === 0) { - return { ...stop, color: `rgba(0, 0, 0, 1)` } - } else { - return { ...stop, color: `rgba(255, 255, 255, ${color.alpha})` } - } - }) - } - return stops } @@ -452,14 +441,8 @@ export default async function backgroundImage( const [src, imageWidth, imageHeight] = await resolveImageData( image.slice(4, -1) ) - const resolvedWidth = - from === 'mask' - ? imageWidth || dimensionsWithoutFallback[0] - : dimensionsWithoutFallback[0] || imageWidth - const resolvedHeight = - from === 'mask' - ? imageHeight || dimensionsWithoutFallback[1] - : dimensionsWithoutFallback[1] || imageHeight + const resolvedWidth = dimensionsWithoutFallback[0] || imageWidth + const resolvedHeight = dimensionsWithoutFallback[1] || imageHeight return [ `satori_bi${id}`, diff --git a/src/builder/mask-image.ts b/src/builder/mask-image.ts index 6bd72c69..2325e42e 100644 --- a/src/builder/mask-image.ts +++ b/src/builder/mask-image.ts @@ -1,6 +1,6 @@ import { buildXMLString } from '../utils.js' import buildBackgroundImage from './background-image.js' -import type { MaskProperty } from '../parser/mask.js' +import type { MaskParsedRes } from '../parser/mask.js' const genMaskImageId = (id: string) => `satori_mi-${id}` @@ -17,15 +17,16 @@ export default async function buildMaskImage( ): Promise<[string, string]> { if (!style.maskImage) return ['', ''] const { left, top, width, height, id } = v - const maskImage = style.maskImage as unknown as MaskProperty[] - const length = maskImage.length + const maskImage = style.maskImage as unknown as MaskParsedRes + const images = maskImage.detail + const length = images.length if (!length) return ['', ''] const miId = genMaskImageId(id) let mask = '' for (let i = 0; i < length; i++) { - const m = maskImage[i] + const m = images[i] const [_id, def] = await buildBackgroundImage( { id: `${miId}-${i}`, left, top, width, height }, @@ -45,7 +46,19 @@ export default async function buildMaskImage( }) } - mask = buildXMLString('mask', { id: miId }, mask) + mask = buildXMLString( + 'mask', + { + id: miId, + // FIXME: although mask-type's default value is luminance, but we can get the same result with what browser renders unless + // i set mask-type with alpha + style: [ + `mask-type: ${maskImage.type}`, + `-webkit-mask-type: ${maskImage.type}`, + ].join(';'), + }, + mask + ) return [miId, mask] } diff --git a/src/handler/expand.ts b/src/handler/expand.ts index 92ba9d9f..4b5ef509 100644 --- a/src/handler/expand.ts +++ b/src/handler/expand.ts @@ -13,7 +13,7 @@ import parseTransformOrigin, { ParsedTransformOrigin, } from '../transform-origin.js' import { isString, lengthToNumber, v, splitEffects } from '../utils.js' -import { MaskProperty, parseMask } from '../parser/mask.js' +import { MaskParsedRes, parseMask } from '../parser/mask.js' import { FontWeight, FontStyle } from '../font.js' // https://react-cn.github.io/react/tips/style-props-value-px.html @@ -227,7 +227,7 @@ type MainStyle = { color: string fontSize: number transformOrigin: ParsedTransformOrigin - maskImage: MaskProperty[] + maskImage: MaskParsedRes opacity: number textTransform: string whiteSpace: string diff --git a/src/parser/mask.ts b/src/parser/mask.ts index 60ecf93a..86513c7e 100644 --- a/src/parser/mask.ts +++ b/src/parser/mask.ts @@ -3,7 +3,9 @@ import { splitEffects } from '../utils.js' function getMaskProperty(style: Record, name: string) { const key = getPropertyName(`mask-${name}`) - return (style[key] || style[`WebkitM${key.substring(1)}`]) as string + return ( + (style[key] || style[`WebkitM${key.substring(1)}`] || '') as string + ).split(',') } export interface MaskProperty { @@ -13,25 +15,53 @@ export interface MaskProperty { repeat: string origin: string clip: string + mode: string +} + +export interface MaskParsedRes { + type: string + detail: MaskProperty[] } export function parseMask( style: Record -): MaskProperty[] { +): MaskParsedRes { const maskImage = (style.maskImage || style.WebkitMaskImage) as string const common = { - position: getMaskProperty(style, 'position') || '0% 0%', - size: getMaskProperty(style, 'size') || '100% 100%', - repeat: getMaskProperty(style, 'repeat') || 'repeat', - origin: getMaskProperty(style, 'origin') || 'border-box', - clip: getMaskProperty(style, 'origin') || 'border-box', + position: getMaskProperty(style, 'position'), + size: getMaskProperty(style, 'size'), + repeat: getMaskProperty(style, 'repeat'), + origin: getMaskProperty(style, 'origin'), + clip: getMaskProperty(style, 'origin'), + mode: getMaskProperty(style, 'mode'), } - let maskImages = splitEffects(maskImage).filter((v) => v && v !== 'none') + const images = splitEffects(maskImage).filter((v) => v && v !== 'none') + + const result = [] - return maskImages.reverse().map((m) => ({ - image: m, - ...common, - })) + for (let i = 0, n = images.length; i < n; i++) { + result[i] = { + image: images[i], + position: common.position[i] || '0% 0%', + size: common.size[i] || '', + repeat: common.repeat[i] || 'repeat', + origin: common.origin[i] || 'border-box', + clip: common.clip[i] || 'border-box', + // https://drafts.fxtf.org/css-masking/#the-mask-mode + // match-source(default), alpha, luminance + // image -> alpha: + // 1. url() + // 2.gradient + // mask-source -> luminance(e.g url(mask#id)) + // we do rarely use mask-source in satori, so here we just set alpha by default + mode: common.mode[i] || 'alpha', + } + } + + return { + type: (getMaskProperty(style, 'type')[0] || 'alpha') as string, + detail: result, + } } diff --git a/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-render-correctly-with-real-image-as-mask-image-1-snap.png b/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-render-correctly-with-real-image-as-mask-image-1-snap.png new file mode 100644 index 00000000..c82962b0 Binary files /dev/null and b/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-render-correctly-with-real-image-as-mask-image-1-snap.png differ diff --git a/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-image-1-snap.png b/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-image-1-snap.png index 5db2f2b6..8016a4fe 100644 Binary files a/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-image-1-snap.png and b/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-image-1-snap.png differ diff --git a/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-image-2-snap.png b/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-image-2-snap.png index 90de1fc0..339d6498 100644 Binary files a/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-image-2-snap.png and b/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-image-2-snap.png differ diff --git a/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-image-on-img-1-snap.png b/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-image-on-img-1-snap.png index 863af21f..954db26d 100644 Binary files a/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-image-on-img-1-snap.png and b/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-image-on-img-1-snap.png differ diff --git a/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-image-on-text-1-snap.png b/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-image-on-text-1-snap.png index 117ba597..8e3756e7 100644 Binary files a/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-image-on-text-1-snap.png and b/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-image-on-text-1-snap.png differ diff --git a/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-position-1-snap.png b/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-position-1-snap.png index d660bbd4..9eaad936 100644 Binary files a/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-position-1-snap.png and b/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-position-1-snap.png differ diff --git a/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-repeat-1-snap.png b/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-repeat-1-snap.png index 147ce8fa..8a1958e6 100644 Binary files a/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-repeat-1-snap.png and b/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-repeat-1-snap.png differ diff --git a/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-size-1-snap.png b/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-size-1-snap.png index fbfc5b2f..f3799982 100644 Binary files a/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-size-1-snap.png and b/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-mask-size-1-snap.png differ diff --git a/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-multiple-mask-image-1-snap.png b/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-multiple-mask-image-1-snap.png index e9a4e3e5..bd2ca415 100644 Binary files a/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-multiple-mask-image-1-snap.png and b/test/__image_snapshots__/mask-image-test-tsx-test-mask-image-test-tsx-mask-should-support-multiple-mask-image-1-snap.png differ diff --git a/test/mask-image.test.tsx b/test/mask-image.test.tsx index 5afafa6e..cf68ec23 100644 --- a/test/mask-image.test.tsx +++ b/test/mask-image.test.tsx @@ -136,6 +136,31 @@ describe('Mask-*', () => { ) expect(toImage(svg, 100)).toMatchImageSnapshot() }) + + it('should render correctly with real image as mask-image', async () => { + const svg = await satori( +
, + { width: 100, height: 100, fonts } + ) + + expect(toImage(svg, 100)).toMatchImageSnapshot() + }) it('should support mask-position', async () => { const svg = await satori(