Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: upgrade resvg to ^2.6.0 & refine mask-image #572

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"homepage": "https://github.com/vercel/satori#readme",
"devDependencies": {
"@arethetypeswrong/cli": "^0.4.2",
"@resvg/resvg-js": "^2.1.0",
"@resvg/resvg-js": "^2.6.0",
"@types/node": "^16",
"@types/opentype.js": "^1.3.3",
"@types/react": "^17.0.38",
Expand Down
1,944 changes: 1,052 additions & 892 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

21 changes: 2 additions & 19 deletions src/builder/background-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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}`,
Expand Down
24 changes: 18 additions & 6 deletions src/builder/mask-image.ts
Original file line number Diff line number Diff line change
@@ -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}`

Expand All @@ -16,16 +16,19 @@ export default async function buildMaskImage(
inheritedStyle: Record<string, string | number>
): 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)

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 },
Expand All @@ -45,7 +48,16 @@ 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
'mask-type': maskImage.type,
},
mask
)

return [miId, mask]
}
4 changes: 2 additions & 2 deletions src/handler/expand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -227,7 +227,7 @@ type MainStyle = {
color: string
fontSize: number
transformOrigin: ParsedTransformOrigin
maskImage: MaskProperty[]
maskImage: MaskParsedRes
opacity: number
textTransform: string
whiteSpace: string
Expand Down
54 changes: 42 additions & 12 deletions src/parser/mask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { splitEffects } from '../utils.js'

function getMaskProperty(style: Record<string, string | number>, 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 {
Expand All @@ -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<string, string | number>
): 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,
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions test/mask-image.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,29 @@ describe('Mask-*', () => {
)
expect(toImage(svg, 100)).toMatchImageSnapshot()
})

it('should render correctly with real image as mask-image', async () => {
const svg = await satori(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
fontSize: 32,
fontWeight: 600,
background: 'red',
maskImage:
'url(https://fxbssdl.kgimg.com/bss/fxams/f2846cbe8d1c89ce84191b5c05ce1df9.png)',
maskSize: '50px 50px',
maskRepeat: 'no-repeat', // just for reference in html
}}
></div>,
{ width: 100, height: 100, fonts }
)

expect(toImage(svg, 100)).toMatchImageSnapshot()
})
})