Skip to content

Commit f4729fe

Browse files
Copilotshuding
andauthored
fix: Support xlinkHref attribute for SVG image elements (#724)
SVG `<image>` elements only recognized `href` as the image source attribute, causing errors when using the JSX/React-compatible `xlinkHref` prop. ```jsx // This worked <svg><image href="data:image/..." /></svg> // This threw "Image source is not provided" <svg><image xlinkHref="data:image/..." /></svg> ``` ## Changes **src/handler/preprocess.ts** - Line 125: Check both `href` and `xlinkHref` when resolving image data from cache - Lines 159-166: Collect image sources from both `props.href` and `props.xlinkHref` during preprocessing **test/image.test.tsx** - Added test case with `xlinkHref` attribute including `xmlns:xlink` namespace declaration The existing `ATTRIBUTE_MAPPING` already handles the `xlinkHref` → `xlink:href` conversion for SVG output. <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> ---- *This section details on the original issue you should resolve* <issue_title>`xlinkHref` isn't interpreted as source for `<image />` in SVGs</issue_title> <issue_description># Bug report ## Description / Observed Behavior Using namespaced attribute in JSX form like `xlinkHref` instead of `xlink:href` causes error "Image source is not provided" to be thrown. ## Expected Behavior Satori should use the JSX/TSX attribute `xlinkHref` as the `<image />` source, interpreting it the same it does `xlink:href` and `href`. ## Reproduction [Using `xlink:href` (works)](https://og-playground.vercel.app/?share=5Vlpb-LIFv0riNGT-sk98b6Q6R7JgFlsY8AbGEUaecM2eF-BVv_3VyaQkPQoetJLPryJpURVp67r3nN8q-pi_-jaieN277vfnKB-iDudojyG7vcfP9p2p-O7geeX952HLoog_3rofn2Em8Ap_V9QJyjS0Dy2-DZ0D0942xkGuWuXQRK3o3YSVlH8NG6GgRdPSzcqzoNuXLr50-CuKspgexwkAI3Pkbwy2IIRJTi59x0SxW6w1SV0CkEuaOkeSrb19cssP38-xH-2jW9F7d1Q_P7QxRHkoXsrxkusDtymnxwAiHSQDhho_66D2yAMwUicxO4VOkRhXADML8v0Hoabprlr8Lsk92AMQRC4dX9reX8Ig3j_t_Zor9eDz8OPd5zjBwxanV9G_yrwa1iNH5Qgrg58e-djE3gHBmiPvMPo3jWgTucIQIK4w2kKewavrhj6GXtyeAvm5znRO_IZKnMzLrZJHoGRPCnN0v1CkJ2L387F1b-f7S-RV3n45bfULMEDjJG_COwvlMJvrIoyT_YusLNC076o84z__qTNdeT_WwD0AwXASPoOpYiXAqAIAG-wd-bfQzoXt51HT2_Txz6UPnVHEk92F_qvk-Kd-aM4CQQ4O74mwtsK4B-6AvA7-vUK-CUr3lmB31EGAdTPnq-58LYExEdKgJB3r5fAL4nx7gqAJHh0fM2FtwUgP1AAAjwIkniVA78kxnsrALaBi-NrKrytAPWRCrze8h-3wZd58d4CgHPg4veSCW_zpz90Cbzc8c_H4Ous-F_4_w0f5h35OO62uLRB7-Lg2u90Agfc9aqceJ6907kMXUpQLQ7KtoJLrB2QqZ9UsRPEHqgBb2-5xoLegk9q3KBPYYHAqsLtnAu6ez93t8DutyAyPfc5pBd5Uthm6H5B7hD0fAGdngiDueBL0P8l7WsR8cloX4uHT0b7WjF8MtrXKuGT0b7WBp-M9rUg-GS0r3XAJ6N9LRf-ebTPc70i_Wr-X-PvPb2DekHgJZzmbuHmtcsWKVBENsHrOWBz-7aqvV4wc8zSvD97h9PY-8MyC5civgZ6fy43iDD2EhZckqL5nOaB1qjt980Ba7Q4kXHcoG0M1v3paj0DrUIF_0Su4dgobc5G7FpSZGTK5gVhU8sWkOOlhvbBXYddUzPGUgPYMlQ0ua9PvD69OVlKIPW56SYnCiVB1tqolqc6t-mxDBtrA3iCZGzquoMRQTclwa8D4sQm0aFY2hu6GvPcgcqi1dDEEBzCGW-2wQIGsnGHQ1FVrlAMwjhx1hjSppRccieQqjiy1QZDKxinXLwUZccSUj3ZREYkoVSOKj5_KHtHM8oEstrGNj2W5GRGxRroTUaHvSaq68HAY5PDit0bOWvO9WCeo25AbOxc9CZkOUg2QwibKDSuwKNZbA2g_WnCi2PInBJZqOVzah6n5iZzYhsZ1TQEkz1K8xWkEOdysDgRUCbpGWkbJFoTzWKwXVj2zFxNS6nUsxAZyjtKZwdyUPQTcsUiWsql0VLLT9l-lvOGOE03Ky3x1gIvrDZxXFqz44kJTxg-HFBbnarWXBVqC3XCFSpDU32Hi4cTdGhKZD9YmzwpIm17UI1HDeUumECaocY8VYAfQHWgB5l-2CfBTthpccir0NpI3Bpbs8reNmJUHfE5C5maMc1N21KSpTLTPX4sNONdaPgz_2T7zBzlKgll5dN8NQx0r-zz_bGOEtvMDGMZE0ra5Sc-k_LT6SHao-5iWO0y046D5MQzDulq481EPDJ1tVKnR21hrufOfophbAVvwtRU2UKfkgK5ghIa3RvQDmm0UWbzFnTwLP1UQGlN01WMGSJJmQrK696aL6o9o5qMSUoRvESZBpv7R8MyiO2WqGlz7K9ndFPFKbpEN-ViFe4g07ei3nqXBX7jCj3YmfCxW-eIWxdWWEgBE9aC2kwYOa6HKxOFVIVwx8ZxGcJJs0VOE0GWrJJx5ioaVViIODHHk3uGH9eYwI2hOhUWh9HJxxbDcenIHjnh6Li0dyiNpkh5rHZbv7DmcSXuNHKmmVieEclKx0Oan0N6rsZ4edxDlRbCqkcqNjq3Iipx8GOR7yKR4rGaX_uIrY0lXEezE4NJOwWqc0FZTMzZQYFgKC4zvOwtsSODwUhxJJR6yDiGF_PzTTbdjQNFZktEXYgyXpLRuhnxlqfMRgl4yCi3GeZC-1wiPcusTKaPvr9Oc0M9CgpIGvo4ZSd6zlHEicw21RgNLcKySF0UcWlFhbAhVROtPm0RfFW7s4W1raN8hAhsmztDWQHpDrI79_JSsCdqf7nMnnNLZvMlQqzyTHK0vaQE0Wpfmdq8UOqVY8uzoHYc-3gIzYDwSaqRHGIjqnk9gsvYYXqivSfNExJqrMHAZYUZxeQx10FuTptVEnvDEofXo1SYkLVdsSg3Oyz2ybbqM0NIOJgTGrzWN_I21wNxuoTdyu0fqRM81Hcxf2AWh3WqCb7J0ds9wQqPWiHcbmQMfIjSG2FE8_aLNdauCX8iZvieS7cTMVkbeY_yhih30j0qnF60sBrG4eERqi0lVZUEe7CWVsNM86QRkMQLDXGpNDJGOVDMJutKBEFu4ZOPDLEBMtqBPV0OysMYGrOHirXJaNFr2r26z8sayeV73vO87-C8uB4jTz_F4aff4t_ajx2gBaCg_vOPh7j7tZuk7Seionv_o3s-ybr37Ted7uP5dek4rlV53futGRbu164bJbtAPabtJ6xtWIFzHkzTfv7hIst1uvdlXrk_v3ZL0wIGvhuGSZPkodP9-R8) [Using `xlinkHref` (doesn't work)](https://og-playground.vercel.app/?share=5Vlpb-LIFv0riNGT-sk98YI3Mt0jGTCLbQx4w0aRRt6wDd5XoNX__ZUJJCQ9ip70kg9vYilR1anruvcc36q62D-6duK43fvuNyeoH-JOpyiPofv9x4-23en4buD55X3noYsiyL8eul8f4SZwSv8X1AmKNDSPLb4N3cMT3nZGQe7aZZDE7aidhFUUP42bYeDFs9KNivOgG5du_jS4q4oy2B6HCUDjcySvDLZgRA5O7n2HQLEbbH0JnUSQC1q6h5Jpff0yy8-fD_GfbeNbUXs3FL8_dHsI8tC9FeMlVgduM0gOAEQ6SAcMtH_XwW0QhmAkTmL3Ch2iMC4A5pdleg_DTdPcNb27JPdgDEEQuHV_a6mHQbz_W3O03-_Dh3b48YZz-IBAK_PL4F_FfY2q8YMShNWBb-98bALnwADtE3cY1b_G0-kcAYjjdz2KxJ7BqyuaesaeHN6C-XlO9I54hsrcjIttkkdgJE9Ks3S_4ETn4rdzcfXvZ_tL5FUefvktNUvw_GLkLxz7CyV7N1ZFmSd7F9hZoWlf1HnGf3_S5jry_y0A-oECYAR1h5L4SwFQBIA32Dvz7yOdi9vOo6e36WMfSp-8I_Anuwv910nxzvzRHgEEODu-JsLbCvQ-dAX07qjXK-CXrHhnBX5HaQRQP3u-5sLbEuAfKQFC3L1eAr8kxrsrAJLg0fE1F94WgPhAAXDwIAj8VQ78khjvrQDYBi6Or6nwtgLkRyrwest_3AZf5sV7CwDOgYvfSya8zZ_60CXwcsc_H4Ovs-J_4f83fOh35OO42-LSBr2Lg2u_0wkccNercuJ59k7nMnSpQNU4KNsCLrF2QKZBUsVOEHugBLy95RoLegs-qXGDPoUFAqsKt3Mu6Ka5uwVmvwWR6bnPEb1Ik8I2Q_cLcoeg5wvI9MQXTAVfYv4vWV9riM_F-lo6fC7W13Lhc7G-Vgifi_W1LPhcrK-lwOdifS0APhfra5nwj2N9nusV51fz_xp-_-nN04v4X8Jp7hZuXrtMkQJBJBO8lAM2t--o2uuWmGOW5v3ZOZzG3h-WWbgk_jXQBgupQfiJlzDgEmXVZ1UPtMZtf2AOGaPF8Yxlh21jqA9ma30OWoUC_glswzJR2pyNGF2UJWTG5AVuk6sWkOKVig7AXYddU9PGSgXYKpRVaaBNvQG1OVlyIA7Y2SbHCzlBdHVcSzON3fQZmonVITxFMiZ13eEYp5oS5_QAPzFJdChW9oaqJhx7ILNoPTIxpAf1aG--wQIasnsOi6KKVKEYhLHCvDHETSm6xI4nFGFsKw2GVnCPdHulIDkWn2rJJjIiESVzVPa5Q9k_mlHGE9U2tqmJKCVzMlZBbzo-7FVB0YdDj0kOa2Zv5Iy50IJFjroBvrFzwZsS5TDZjCBsKlM9GR7PY2sI7U9TTphA5gzPQjVfkIs4NTeZE9vIuKYgmOiTqi8jhbCQguUJhzJRywjbINAab5bD7dKy5-Z6VoqlloXISNqRGjOUgmKQEGsGUVM2jVZqfsr285wzhFm6WauJp_M... </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes #722 <!-- START COPILOT CODING AGENT TIPS --> --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: shuding <3676859+shuding@users.noreply.github.com>
1 parent 7c08d3a commit f4729fe

File tree

3 files changed

+44
-5
lines changed

3 files changed

+44
-5
lines changed

src/handler/preprocess.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ function translateSVGNodeToSVGString(
122122
_v = currentColor
123123
}
124124
125-
if (k === 'href' && type === 'image') {
125+
if ((k === 'href' || k === 'xlinkHref') && type === 'image') {
126126
return ` ${ATTRIBUTE_MAPPING[k] || k}="${cache.get(_v as string)[0]}"`
127127
}
128128
return ` ${ATTRIBUTE_MAPPING[k] || k}="${_v}"`
@@ -156,10 +156,13 @@ export async function preProcessNode(node: ReactNode) {
156156
return
157157
} else if (typeof _node === 'object') {
158158
if (_node.type === 'image') {
159-
if (set.has(_node.props.href)) {
160-
// do nothing
161-
} else {
162-
set.add(_node.props.href)
159+
const imageSrc = _node.props.href || _node.props.xlinkHref
160+
if (imageSrc) {
161+
if (set.has(imageSrc)) {
162+
// do nothing
163+
} else {
164+
set.add(imageSrc)
165+
}
163166
}
164167
} else if (_node.type === 'img') {
165168
if (set.has(_node.props.src)) {
2.94 KB
Loading

test/image.test.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,42 @@ describe('Image', () => {
138138
expect(toImage(svg, 100)).toMatchImageSnapshot()
139139
})
140140

141+
it('should render svg with image using xlinkHref', async () => {
142+
const svg = await satori(
143+
<div
144+
style={{
145+
height: '100%',
146+
width: '100%',
147+
display: 'flex',
148+
flexDirection: 'column',
149+
alignItems: 'center',
150+
justifyContent: 'center',
151+
backgroundColor: '#fff',
152+
fontSize: 32,
153+
fontWeight: 600,
154+
}}
155+
>
156+
<svg
157+
width='100'
158+
height='100'
159+
viewBox='0 0 100 100'
160+
fill='none'
161+
xmlns='http://www.w3.org/2000/svg'
162+
xmlnsXlink='http://www.w3.org/1999/xlink'
163+
>
164+
<image
165+
id='image0_1_3'
166+
width='100'
167+
height='100'
168+
xlinkHref='data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiI+PHBhdGggZmlsbD0iI0ZGQ0M0RCIgZD0iTTM2IDE4YzAgOS45NDEtOC4wNTkgMTgtMTggMTgtOS45NCAwLTE4LTguMDU5LTE4LTE4QzAgOC4wNiA4LjA2IDAgMTggMGM5Ljk0MSAwIDE4IDguMDYgMTggMTgiLz48ZWxsaXBzZSBmaWxsPSIjNjY0NTAwIiBjeD0iMTEuNSIgY3k9IjEyLjUiIHJ4PSIyLjUiIHJ5PSI1LjUiLz48ZWxsaXBzZSBmaWxsPSIjNjY0NTAwIiBjeD0iMjQuNSIgY3k9IjEyLjUiIHJ4PSIyLjUiIHJ5PSI1LjUiLz48cGF0aCBmaWxsPSIjNjY0NTAwIiBkPSJNMTggMjJjLTMuNjIzIDAtNi4wMjctLjQyMi05LTEtLjY3OS0uMTMxLTIgMC0yIDIgMCA0IDQuNTk1IDkgMTEgOSA2LjQwNCAwIDExLTUgMTEtOSAwLTItMS4zMjEtMi4xMzItMi0yLTIuOTczLjU3OC01LjM3NyAxLTkgMXoiLz48cGF0aCBmaWxsPSIjRkZGIiBkPSJNOSAyM3MzIDEgOSAxIDktMSA5LTEtMiA0LTkgNC05LTQtOS00eiIvPjwvc3ZnPg=='
169+
/>
170+
</svg>
171+
</div>,
172+
{ width: 100, height: 100, fonts }
173+
)
174+
expect(toImage(svg, 100)).toMatchImageSnapshot()
175+
})
176+
141177
it('should throw error when relative path is used', async () => {
142178
await expect(
143179
satori(

0 commit comments

Comments
 (0)