Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions apps/web/src/services/export/wechat-svg.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,48 @@ describe(`prepareDiagramSvgsForWeChat`, () => {
expect(stroke).toMatch(/#|rgb/i)
expect(root.querySelector(`path`)?.getAttribute(`fill`)).toBe(`none`)
})


it(`converts embedded AntV gradient and transparent pattern for WeChat`, () => {
const embedded = `
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="card-gradient">
<stop offset="0%" stop-color="#026bff"/>
<stop offset="100%" stop-color="#539bff"/>
</linearGradient>
<pattern id="card-pattern" width="10" height="10">
<rect width="10" height="10" fill="rgba(255, 255, 255, 0)"/>
<rect width="4" height="10" fill="rgba(0, 0, 0, 0.03)"/>
</pattern>
</defs>
</svg>
`
const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(embedded)}`

const root = document.createElement(`div`)
const svg = document.createElementNS(`http://www.w3.org/2000/svg`, `svg`)
const base = document.createElementNS(svg.namespaceURI, `rect`)
const overlay = document.createElementNS(svg.namespaceURI, `rect`)

for (const rect of [base, overlay]) {
rect.setAttribute(`x`, `0`)
rect.setAttribute(`y`, `0`)
rect.setAttribute(`width`, `280`)
rect.setAttribute(`height`, `160`)
svg.appendChild(rect)
}

base.setAttribute(`fill`, `url("${dataUrl}#card-gradient")`)
overlay.setAttribute(`fill`, `url("${dataUrl}#card-pattern")`)
root.appendChild(svg)

prepareDiagramSvgsForWeChat(root)

expect(base.getAttribute(`fill`)).toMatch(/^rgb\(|^#/)
expect(overlay.getAttribute(`fill`)).toBe(`none`)
})

})

describe(`sanitizeSvgsForWeChat`, () => {
Expand Down
99 changes: 96 additions & 3 deletions apps/web/src/services/export/wechat-svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,23 +260,111 @@ function mixCssColors(a: string, b: string): string | null {
return `rgb(${Math.round((pa[0] + pb[0]) / 2)}, ${Math.round((pa[1] + pb[1]) / 2)}, ${Math.round((pa[2] + pb[2]) / 2)})`
}

function embeddedSvgPaintServer(paint: string): Element | null {
const value = paint.trim()
if (!value.startsWith(`url(`) || !value.endsWith(`)`))
return null

let url = value.slice(4, -1).trim()

if (
(url.startsWith(`"`) && url.endsWith(`"`))
|| (url.startsWith(`'`) && url.endsWith(`'`))
) {
url = url.slice(1, -1)
}

if (!url.toLowerCase().startsWith(`data:image/svg+xml`))
return null

const comma = url.indexOf(`,`)
const hash = url.lastIndexOf(`#`)
if (comma < 0 || hash <= comma)
return null

if (/;base64/i.test(url.slice(0, comma)))
return null

try {
const markup = decodeURIComponent(url.slice(comma + 1, hash))
const id = decodeURIComponent(url.slice(hash + 1))
const doc = new DOMParser().parseFromString(markup, `image/svg+xml`)

if (doc.querySelector(`parsererror`))
return null

return Array.from(doc.querySelectorAll(`[id]`))
.find(el => el.getAttribute(`id`) === id) ?? null
}
catch {
return null
}
}

function isTransparentOverlayPattern(ref: Element): boolean {
if (ref.localName !== `pattern`)
return false

const shapes = Array.from(ref.children)
if (shapes.length === 0)
return false

return shapes.every((shape) => {
if (shape.localName !== `rect`)
return false

const fill = shape.getAttribute(`fill`) ?? ``
const rgba = fill.match(/^rgba\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*,\s*([\d.]+)\s*\)$/i)
return fill === `none` || (rgba !== null && Number(rgba[1]) <= 0.1)
})
}

function hasMatchingPaintedRectBelow(el: Element): boolean {
if (el.localName !== `rect`)
return false

let previous = el.previousElementSibling

while (previous) {
const candidate = previous

if (
candidate.localName === `rect`
&& [`x`, `y`, `width`, `height`, `rx`, `ry`]
.every(attr => candidate.getAttribute(attr) === el.getAttribute(attr))
) {
const fill = candidate.getAttribute(`fill`)
return !!fill && fill !== `none` && !fill.includes(`url(`)
}

previous = candidate.previousElementSibling
}

return false
}

/** WeChat strips defs/id, so url(#gradient) paints vanish. Bake a solid stand-in. */
function solidColorFromPaintServer(svg: SVGSVGElement, paint: string): string | null {
const embedded = embeddedSvgPaintServer(paint)
const id = parseMarkerRef(paint)
if (!id)
return null
const ref = findSvgElementById(svg, id)

const ref = embedded ?? (id ? findSvgElementById(svg, id) : null)
if (!ref)
return null

if (ref.localName !== `linearGradient` && ref.localName !== `radialGradient`)
return null

const colors = Array.from(ref.querySelectorAll(`stop`))
.map(stopColorOf)
.filter((color): color is string => !!color)

if (colors.length === 0)
return null

if (colors.length === 1)
return colors[0]

return mixCssColors(colors[0], colors[colors.length - 1]) ?? colors[0]
}

Expand All @@ -285,6 +373,11 @@ function resolveUrlPaints(svg: SVGSVGElement) {
const value = el.getAttribute(attr)
if (!value?.includes(`url(`))
return
const embedded = embeddedSvgPaintServer(value)
if (embedded && isTransparentOverlayPattern(embedded) && hasMatchingPaintedRectBelow(el)) {
el.setAttribute(attr, `none`)
return
}
const solid = solidColorFromPaintServer(svg, value)
if (solid)
el.setAttribute(attr, solid)
Expand Down
Loading