From 81a78ce10037c91bd54dd443e3244c2463fb9dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=A0=E6=89=8B=E6=8D=A7=E9=B2=9C=E8=8A=B1?= <157215725@qq.com> Date: Wed, 26 Apr 2023 18:28:57 +0800 Subject: [PATCH] feat: support svg href --- src/clone-element.ts | 13 +++++++++- src/clone-svg.ts | 30 +++++++++++++++++++++++ src/context.ts | 5 ++++ src/converts/dom-to-canvas.ts | 6 +++-- src/converts/dom-to-foreign-object-svg.ts | 2 ++ src/create-context.ts | 3 ++- src/destroy-context.ts | 1 + src/utils.ts | 7 ++++-- 8 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 src/clone-svg.ts diff --git a/src/clone-element.ts b/src/clone-element.ts index d41e9fa..e8cbe05 100644 --- a/src/clone-element.ts +++ b/src/clone-element.ts @@ -1,4 +1,11 @@ -import { isCanvasElement, isIFrameElement, isImageElement, isVideoElement } from './utils' +import { cloneSvg } from './clone-svg' +import { + isCanvasElement, + isIFrameElement, + isImageElement, + isSVGSVGElementNode, + isVideoElement, +} from './utils' import { cloneIframe } from './clone-iframe' import { cloneCanvas } from './clone-canvas' import { cloneVideo } from './clone-video' @@ -25,5 +32,9 @@ export function cloneElement( return cloneVideo(node) } + if (isSVGSVGElementNode(node)) { + return cloneSvg(node, context) + } + return node.cloneNode(false) as T } diff --git a/src/clone-svg.ts b/src/clone-svg.ts new file mode 100644 index 0000000..0490c77 --- /dev/null +++ b/src/clone-svg.ts @@ -0,0 +1,30 @@ +import { cloneNode } from './clone-node' +import type { Context } from './context' + +export async function cloneSvg( + node: T, + context: Context, +): Promise { + const { ownerDocument, svgDefsElement } = context + + const uses = node.querySelectorAll?.('use') ?? [] + + if (uses.length) { + for (let len = uses.length, i = 0; i < len; i++) { + const use = uses[i] + + const id = use.getAttribute('xlink:href') + ?? use.getAttribute('href') + + if (!id) continue + + const definition = ownerDocument?.querySelector(`svg ${ id }`) + + if (!definition || svgDefsElement?.querySelector(id)) continue + + svgDefsElement?.appendChild(await cloneNode(definition, context)) + } + } + + return node.cloneNode(false) as T +} diff --git a/src/context.ts b/src/context.ts index fdc750e..0483f60 100644 --- a/src/context.ts +++ b/src/context.ts @@ -49,6 +49,11 @@ export interface InternalContext { */ svgStyleElement?: HTMLStyleElement + /** + * The `defs` element under the root `svg` element + */ + svgDefsElement?: SVGDefsElement + /** * The `svgStyleElement` class styles * diff --git a/src/converts/dom-to-canvas.ts b/src/converts/dom-to-canvas.ts index fdd51fd..e96e759 100644 --- a/src/converts/dom-to-canvas.ts +++ b/src/converts/dom-to-canvas.ts @@ -1,9 +1,9 @@ import { createStyleElement, orCreateContext } from '../create-context' -import { createImage, svgToDataUrl } from '../utils' +import { createImage, svgToDataUrl, xmlns } from '../utils' import { imageToCanvas } from '../image-to-canvas' import { domToForeignObjectSvg } from './dom-to-foreign-object-svg' -import type { Context } from '../context' +import type { Context } from '../context' import type { Options } from '../options' export async function domToCanvas(node: T, options?: Options): Promise @@ -14,6 +14,8 @@ export async function domToCanvas(node: any, options?: any) { const dataUrl = svgToDataUrl(svg) if (!context.autoDestruct) { context.svgStyleElement = createStyleElement(context.ownerDocument) + context.svgDefsElement = context.ownerDocument?.createElementNS(xmlns, 'defs') + context.svgStyles.clear() } const image = createImage(dataUrl, svg.ownerDocument) return await imageToCanvas(image, context) diff --git a/src/converts/dom-to-foreign-object-svg.ts b/src/converts/dom-to-foreign-object-svg.ts index 3c068d4..79958cd 100644 --- a/src/converts/dom-to-foreign-object-svg.ts +++ b/src/converts/dom-to-foreign-object-svg.ts @@ -24,6 +24,7 @@ export async function domToForeignObjectSvg(node: any, options?: any) { log, tasks, svgStyleElement, + svgDefsElement, svgStyles, font, progress, @@ -75,6 +76,7 @@ export async function domToForeignObjectSvg(node: any, options?: any) { onEmbedNode?.(clone) const svg = createForeignObjectSvg(clone, context) + svgDefsElement && svg.insertBefore(svgDefsElement, svg.children[0]) svgStyleElement && svg.insertBefore(svgStyleElement, svg.children[0]) autoDestruct && destroyContext(context) diff --git a/src/create-context.ts b/src/create-context.ts index dbadd93..b84a835 100644 --- a/src/create-context.ts +++ b/src/create-context.ts @@ -5,7 +5,7 @@ import { SUPPORT_WEB_WORKER, isContext, isElementNode, - supportWebp, waitUntilLoad, + supportWebp, waitUntilLoad, xmlns, } from './utils' import type { Context, Request } from './context' import type { Options } from './options' @@ -63,6 +63,7 @@ export async function createContext(node: T, options?: Options & ownerWindow, dpi: scale === 1 ? null : 96 * scale, svgStyleElement: createStyleElement(ownerDocument), + svgDefsElement: ownerDocument?.createElementNS(xmlns, 'defs'), svgStyles: new Map(), defaultComputedStyles: new Map>(), workers: [ diff --git a/src/destroy-context.ts b/src/destroy-context.ts index ed794f6..68d980d 100644 --- a/src/destroy-context.ts +++ b/src/destroy-context.ts @@ -4,6 +4,7 @@ export function destroyContext(context: Context) { context.ownerDocument = undefined context.ownerWindow = undefined context.svgStyleElement = undefined + context.svgDefsElement = undefined context.svgStyles.clear() context.defaultComputedStyles.clear() if (context.sandbox) { diff --git a/src/utils.ts b/src/utils.ts index 0f8f66c..59ff6b1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,7 +21,8 @@ export const isCSSImportRule = (rule: CSSRule): rule is CSSImportRule => rule.co // Element export const isElementNode = (node: Node): node is Element => node.nodeType === 1 // Node.ELEMENT_NODE export const isSVGElementNode = (node: Element): node is SVGElement => typeof (node as SVGElement).className === 'object' -export const isSVGImageElementNode = (node: Element): node is SVGImageElement => isSVGElementNode(node) && node.tagName === 'IMAGE' +export const isSVGSVGElementNode = (node: Element): node is SVGSVGElement => isSVGElementNode(node) && node.tagName === 'svg' +export const isSVGImageElementNode = (node: Element): node is SVGImageElement => isSVGElementNode(node) && node.tagName === 'image' export const isHTMLElementNode = (node: Node): node is HTMLElement => isElementNode(node) && typeof (node as HTMLElement).style !== 'undefined' && !isSVGElementNode(node) export const isCommentNode = (node: Node): node is Text => node.nodeType === 8 // Node.COMMENT_NODE export const isTextNode = (node: Node): node is Text => node.nodeType === 3 // Node.TEXT_NODE @@ -87,8 +88,10 @@ export function getDocument(target?: T | null): Document { ) as any } +export const xmlns = 'http://www.w3.org/2000/svg' + export function createSvg(width: number, height: number, ownerDocument?: Document | null): SVGSVGElement { - const svg = getDocument(ownerDocument).createElementNS('http://www.w3.org/2000/svg', 'svg') + const svg = getDocument(ownerDocument).createElementNS(xmlns, 'svg') svg.setAttributeNS(null, 'width', width.toString()) svg.setAttributeNS(null, 'height', height.toString()) svg.setAttributeNS(null, 'viewBox', `0 0 ${ width } ${ height }`)