diff --git a/src/core/features.ts b/src/core/features.ts index 64e8aea..52a8c51 100644 --- a/src/core/features.ts +++ b/src/core/features.ts @@ -1,4 +1,5 @@ import {fromCodePoint, toCodePoints} from 'css-line-break'; +import {isSVGForeignObjectElement} from '../dom/node-parser'; const testRangeBounds = (document: Document) => { const TEST_HEIGHT = 123; @@ -156,15 +157,37 @@ export const createForeignObjectSVG = ( return svg; }; -export const loadSerializedSVG = (svg: Node): Promise => { - return new Promise((resolve, reject) => { +export const serializeSvg = (svg: SVGSVGElement | SVGForeignObjectElement, encoding: string = ''): string => { + const svgPrefix = 'data:image/svg+xml'; + const selializedSvg = new XMLSerializer().serializeToString(svg); + const encodedSvg = encoding === 'base64' ? btoa(selializedSvg) : encodeURIComponent(selializedSvg); + return `${svgPrefix}${encoding && `;${encoding}`},${encodedSvg}`; +}; + +const INLINE_BASE64 = /^data:image\/.*;base64,/i; +export const deserializeSvg = (svg: string): SVGSVGElement | SVGForeignObjectElement => { + const encodedSvg = INLINE_BASE64.test(svg) ? atob(svg) : decodeURIComponent(svg); + const domParser = new DOMParser(); + const document = domParser.parseFromString(encodedSvg, 'image/svg+xml'); + const parserError = document.querySelector('parsererror'); + if (parserError) { + // @ts-ignore: Expected 0-1 arguments, but got 2. + throw new Error('Deserialisation failed', {cause: parserError}); + } + const {documentElement} = document; + const firstSvgChild = documentElement.firstElementChild; + return firstSvgChild && isSVGForeignObjectElement(firstSvgChild) + ? (documentElement as unknown as SVGForeignObjectElement) + : (documentElement as unknown as SVGSVGElement); +}; + +export const loadSerializedSVG = (svg: SVGSVGElement | SVGForeignObjectElement): Promise => + new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = reject; - - img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`; + img.src = serializeSvg(svg, 'charset=utf-8'); }); -}; export const FEATURES = { get SUPPORT_RANGE_BOUNDS(): boolean { diff --git a/src/dom/node-parser.ts b/src/dom/node-parser.ts index 5f38a24..8e8503c 100644 --- a/src/dom/node-parser.ts +++ b/src/dom/node-parser.ts @@ -122,6 +122,7 @@ export const isOLElement = (node: Element): node is HTMLOListElement => node.tag export const isInputElement = (node: Element): node is HTMLInputElement => node.tagName === 'INPUT'; export const isHTMLElement = (node: Element): node is HTMLHtmlElement => node.tagName === 'HTML'; export const isSVGElement = (node: Element): node is SVGSVGElement => node.tagName === 'svg'; +export const isSVGForeignObjectElement = (node: Element): node is SVGSVGElement => node.tagName === 'foreignObject'; export const isBodyElement = (node: Element): node is HTMLBodyElement => node.tagName === 'BODY'; export const isCanvasElement = (node: Element): node is HTMLCanvasElement => node.tagName === 'CANVAS'; export const isVideoElement = (node: Element): node is HTMLVideoElement => node.tagName === 'VIDEO'; diff --git a/src/dom/replaced-elements/image-element-container.ts b/src/dom/replaced-elements/image-element-container.ts index 51ef1b1..16814c0 100644 --- a/src/dom/replaced-elements/image-element-container.ts +++ b/src/dom/replaced-elements/image-element-container.ts @@ -1,16 +1,42 @@ import {ElementContainer} from '../element-container'; import {Context} from '../../core/context'; +import {serializeSvg, deserializeSvg} from '../../core/features'; export class ImageElementContainer extends ElementContainer { src: string; intrinsicWidth: number; intrinsicHeight: number; + private static INLINE_SVG = /^data:image\/svg\+xml/i; + private static IS_FIRE_FOX = /firefox/i.test(navigator?.userAgent); + constructor(context: Context, img: HTMLImageElement) { super(context, img); this.src = img.currentSrc || img.src; this.intrinsicWidth = img.naturalWidth; this.intrinsicHeight = img.naturalHeight; + this.update(); this.context.cache.addImage(this.src); } + + private update() { + if (!this.intrinsicWidth || !this.intrinsicHeight || ImageElementContainer.IS_FIRE_FOX) { + if (ImageElementContainer.INLINE_SVG.test(this.src)) { + const [, inlinedSvg] = this.src.split(','); + const svgElement = deserializeSvg(inlinedSvg); + + const { + width: {baseVal: widthBaseVal}, + height: {baseVal: heightBaseVal} + } = svgElement; + widthBaseVal.valueAsString = widthBaseVal.value.toString(); + heightBaseVal.valueAsString = heightBaseVal.value.toString(); + this.src = serializeSvg(svgElement, 'base64'); + + this.intrinsicWidth = widthBaseVal.value; + this.intrinsicHeight = heightBaseVal.value; + return; + } + } + } } diff --git a/src/dom/replaced-elements/svg-element-container.ts b/src/dom/replaced-elements/svg-element-container.ts index ae9c5a4..0c5a63a 100644 --- a/src/dom/replaced-elements/svg-element-container.ts +++ b/src/dom/replaced-elements/svg-element-container.ts @@ -1,6 +1,7 @@ import {ElementContainer} from '../element-container'; import {parseBounds} from '../../css/layout/bounds'; import {Context} from '../../core/context'; +import {serializeSvg} from '../../core/features'; export class SVGElementContainer extends ElementContainer { svg: string; @@ -9,12 +10,12 @@ export class SVGElementContainer extends ElementContainer { constructor(context: Context, img: SVGSVGElement) { super(context, img); - const s = new XMLSerializer(); + const bounds = parseBounds(context, img); img.setAttribute('width', `${bounds.width}px`); img.setAttribute('height', `${bounds.height}px`); - this.svg = `data:image/svg+xml,${encodeURIComponent(s.serializeToString(img))}`; + this.svg = serializeSvg(img); this.intrinsicWidth = img.width.baseVal.value; this.intrinsicHeight = img.height.baseVal.value; diff --git a/src/render/canvas/foreignobject-renderer.ts b/src/render/canvas/foreignobject-renderer.ts index a6d1e3a..23fc840 100644 --- a/src/render/canvas/foreignobject-renderer.ts +++ b/src/render/canvas/foreignobject-renderer.ts @@ -1,5 +1,5 @@ import {RenderConfigurations} from './canvas-renderer'; -import {createForeignObjectSVG} from '../../core/features'; +import {createForeignObjectSVG, loadSerializedSVG} from '../../core/features'; import {asString} from '../../css/types/color'; import {Renderer} from '../renderer'; import {Context} from '../../core/context'; @@ -48,13 +48,3 @@ export class ForeignObjectRenderer extends Renderer { } } -export const loadSerializedSVG = (svg: Node): Promise => - new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => { - resolve(img); - }; - img.onerror = reject; - - img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`; - });