Skip to content

Commit

Permalink
fix: svg+xml image decoding in Safari and Firefox (close #15)
Browse files Browse the repository at this point in the history
  • Loading branch information
qq15725 committed Mar 21, 2023
1 parent b250ffa commit 86085a3
Show file tree
Hide file tree
Showing 11 changed files with 63 additions and 49 deletions.
9 changes: 4 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@
}

#root {
display: inline-flex;
font-size: .875rem;
font-family: "Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
line-height: 1.5;
width: 300px;
height: 300px;
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1080 1080"%3E%3Cpath fill="none" d="M0,657C0,438,0,219,0,0c360,0,720,0,1080,0c0,360,0,720,0,1080c-186.7,0-373.3,0-560,0c0.3-1.5,0.4-3.2,0.9-4.6 c19.7-58.2,24.6-117.7,13.7-178.2c-13.7-76.2-48.2-141.9-104.9-194.5c-90.3-83.8-196.9-113-317.7-90.1C71.9,620.2,34.8,635.9,0,657z"%3E%3C/path%3E%3Cpath fill="%23182430" d="M0,657c34.8-21.1,71.9-36.8,112-44.4c120.8-22.9,227.4,6.3,317.7,90.1C486.5,755.4,521,821,534.6,897.2 c10.9,60.5,6,119.9-13.7,178.2c-0.5,1.5-0.6,3.1-0.9,4.6c-173.2,0-346.5,0-520,0C0,939,0,798,0,657z"%3E%3C/path%3E%3C/svg%3E');
}
</style>
</head>
<body>
<div>
<div id="root">前缀 <!---->after</div>
<div id="root"></div>
</div>

<script type="module" async>
Expand Down
6 changes: 3 additions & 3 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ export interface InternalContext<T extends Node> {
requests: Map<string, Request>

/**
* All request images count
* Canvas multiple draw image fix svg+xml image decoding in Safari and Firefox
*/
requestImagesCount: number
drawImageCount: number

/**
* Wait for all tasks embedded in
Expand All @@ -97,7 +97,7 @@ export interface InternalContext<T extends Node> {
/**
* Automatically destroy context
*/
autodestruct: boolean
autoDestruct: boolean
}

export type Context<T extends Node = Node> = InternalContext<T> & Required<Options>
2 changes: 1 addition & 1 deletion src/converts/dom-to-canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export async function domToCanvas(node: any, options?: any) {
const context = await orCreateContext(node, options)
const svg = await domToForeignObjectSvg(context)
const dataUrl = svgToDataUrl(svg)
if (!context.autodestruct) {
if (!context.autoDestruct) {
context.svgStyleElement = createStyleElement(context.ownerDocument)
}
const image = createImage(dataUrl, svg.ownerDocument)
Expand Down
4 changes: 2 additions & 2 deletions src/converts/dom-to-foreign-object-svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export async function domToForeignObjectSvg(node: any, options?: any) {
svgStyleElement,
font,
progress,
autodestruct,
autoDestruct,
onCloneNode,
onEmbedNode,
onCreateForeignObjectSvg,
Expand Down Expand Up @@ -68,7 +68,7 @@ export async function domToForeignObjectSvg(node: any, options?: any) {
const svg = createForeignObjectSvg(clone, context)
svgStyleElement && svg.insertBefore(svgStyleElement, svg.children[0])

autodestruct && destroyContext(context)
autoDestruct && destroyContext(context)

onCreateForeignObjectSvg?.(svg)

Expand Down
8 changes: 4 additions & 4 deletions src/create-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import type { Options } from './options'
export async function orCreateContext<T extends Node>(context: Context<T>): Promise<Context<T>>
export async function orCreateContext<T extends Node>(node: T, options?: Options): Promise<Context<T>>
export async function orCreateContext(node: any, options?: Options): Promise<Context> {
return isContext(node) ? node : createContext(node, { ...options, autodestruct: true })
return isContext(node) ? node : createContext(node, { ...options, autoDestruct: true })
}

export async function createContext<T extends Node>(node: T, options?: Options & { autodestruct?: boolean }): Promise<Context<T>> {
export async function createContext<T extends Node>(node: T, options?: Options & { autoDestruct?: boolean }): Promise<Context<T>> {
const { scale = 1, workerUrl, workerNumber = 1 } = options || {}

const debug = Boolean(options?.debug)
Expand Down Expand Up @@ -52,7 +52,7 @@ export async function createContext<T extends Node>(node: T, options?: Options &
onCloneNode: null,
onEmbedNode: null,
onCreateForeignObjectSvg: null,
autodestruct: false,
autoDestruct: false,
...options,

// InternalContext
Expand Down Expand Up @@ -95,7 +95,7 @@ export async function createContext<T extends Node>(node: T, options?: Options &
'*/*',
].filter(Boolean).join(',') };q=0.8`,
requests,
requestImagesCount: 0,
drawImageCount: 0,
tasks: [],
}

Expand Down
1 change: 0 additions & 1 deletion src/destroy-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export function destroyContext(context: Context) {
context.workers = []
context.fontFamilies.clear()
context.fontCssTexts.clear()
context.requestImagesCount = 0
context.requests.clear()
context.tasks = []
}
4 changes: 4 additions & 0 deletions src/embed-css-style-image.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { replaceCssUrlToDataUrl } from './css-url'
import { IN_FIREFOX, IN_SAFARI } from './utils'
import type { Context } from './context'

const properties = [
Expand All @@ -19,6 +20,9 @@ export function embedCssStyleImage(
if (!value) {
return null
}
if ((IN_SAFARI || IN_FIREFOX) && value.includes('data:image/svg+xml')) {
context.drawImageCount++
}
return replaceCssUrlToDataUrl(value, null, context, true).then(newValue => {
if (!newValue || value === newValue) return
style.setProperty(
Expand Down
45 changes: 27 additions & 18 deletions src/embed-image-element.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,44 @@
import { isDataUrl, isImageElement, isSVGElementNode } from './utils'
import { IN_FIREFOX, IN_SAFARI, isDataUrl, isImageElement, isSVGElementNode } from './utils'
import { contextFetch } from './fetch'
import type { Context } from './context'

export function embedImageElement<T extends HTMLImageElement | SVGImageElement>(
clone: T,
context: Context,
): Promise<void>[] {
if (isImageElement(clone) && !isDataUrl(clone.currentSrc || clone.src)) {
const url = clone.currentSrc || clone.src
clone.srcset = ''
clone.dataset.originalSrc = url
return [
contextFetch(context, {
url,
imageDom: clone,
requestType: 'image',
responseType: 'dataUrl',
}).then(url => {
clone.src = url || ''
}),
]
if (isImageElement(clone)) {
const originalSrc = clone.currentSrc || clone.src

if (!isDataUrl(originalSrc)) {
return [
contextFetch(context, {
url: originalSrc,
imageDom: clone,
requestType: 'image',
responseType: 'dataUrl',
}).then(url => {
if (!url) return
clone.srcset = ''
clone.dataset.originalSrc = originalSrc
clone.src = url || ''
}),
]
}

if ((IN_SAFARI || IN_FIREFOX) && originalSrc.includes('data:image/svg+xml')) {
context.drawImageCount++
}
} else if (isSVGElementNode(clone) && !isDataUrl(clone.href.baseVal)) {
const url = clone.href.baseVal
clone.dataset.originalSrc = url
const originalSrc = clone.href.baseVal
return [
contextFetch(context, {
url,
url: originalSrc,
imageDom: clone,
requestType: 'image',
responseType: 'dataUrl',
}).then(url => {
if (!url) return
clone.dataset.originalSrc = originalSrc
clone.href.baseVal = url || ''
}),
]
Expand Down
6 changes: 3 additions & 3 deletions src/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { blobToDataUrl, consoleWarn } from './utils'
import { IN_SAFARI, blobToDataUrl, consoleWarn } from './utils'
import type { Context, Request } from './context'

export type BaseFetchOptions = RequestInit & {
Expand Down Expand Up @@ -62,8 +62,8 @@ export function contextFetch(context: Context, options: ContextFetchOptions) {
url += (/\?/.test(url) ? '&' : '?') + new Date().getTime()
}

if (requestType === 'image') {
context.requestImagesCount++
if (requestType === 'image' && IN_SAFARI) {
context.drawImageCount++
}

const baseFetchOptions: BaseFetchOptions = {
Expand Down
26 changes: 14 additions & 12 deletions src/image-to-canvas.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IN_SAFARI, consoleWarn, loadMedia } from './utils'
import { consoleWarn, loadMedia } from './utils'
import type { Context } from './context'

export async function imageToCanvas<T extends HTMLImageElement>(
Expand All @@ -7,8 +7,8 @@ export async function imageToCanvas<T extends HTMLImageElement>(
): Promise<HTMLCanvasElement> {
const {
log,
requestImagesCount,
timeout,
drawImageCount,
drawImageInterval,
} = context

Expand All @@ -22,18 +22,20 @@ export async function imageToCanvas<T extends HTMLImageElement>(
consoleWarn('Failed to drawImage', error)
}
}

drawImage()
// fix: image not decode when drawImage svg+xml in safari/webkit
if (IN_SAFARI) {
for (let i = 0; i < requestImagesCount; i++) {
await new Promise<void>(resolve => {
setTimeout(() => {
drawImage()
resolve()
}, i + drawImageInterval)
})
}

for (let i = 0; i < drawImageCount; i++) {
await new Promise<void>(resolve => {
setTimeout(() => {
drawImage()
resolve()
}, i + drawImageInterval)
})
}

context.drawImageCount = 0

log.timeEnd('image to canvas')
return canvas
}
Expand Down
1 change: 1 addition & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const SUPPORT_BTOA = IN_BROWSER && 'btoa' in window
export const USER_AGENT = IN_BROWSER ? window.navigator?.userAgent : ''
export const IN_CHROME = USER_AGENT.includes('Chrome')
export const IN_SAFARI = USER_AGENT.includes('AppleWebKit') && !IN_CHROME
export const IN_FIREFOX = USER_AGENT.includes('Firefox')

// Context
export const isContext = <T extends Node>(value: any): value is Context<T> => value && '__CONTEXT__' in value
Expand Down

1 comment on commit 86085a3

@vercel
Copy link

@vercel vercel bot commented on 86085a3 Mar 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

modern-screenshot – ./

modern-screenshot.vercel.app
modern-screenshot-qq15725.vercel.app
modern-screenshot-git-main-qq15725.vercel.app

Please sign in to comment.