Skip to content

Commit

Permalink
fix: image not decode when drawImage svg+xml in safari/webkit
Browse files Browse the repository at this point in the history
  • Loading branch information
qq15725 committed Feb 2, 2023
1 parent 9023f7b commit f083ddb
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 34 deletions.
4 changes: 2 additions & 2 deletions src/converts/dom-to-svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export async function domToSvg<T extends Node>(
const svg = createSvg(width, height, node.ownerDocument)
const svgImage = svg.ownerDocument.createElementNS(svg.namespaceURI, 'image')
svgImage.setAttributeNS(null, 'href', dataUrl)
svgImage.setAttributeNS(null, 'height', String(width))
svgImage.setAttributeNS(null, 'width', String(height))
svgImage.setAttributeNS(null, 'height', '100%')
svgImage.setAttributeNS(null, 'width', '100%')
svg.appendChild(svgImage)
return svgToDataUrl(svg)
}
19 changes: 14 additions & 5 deletions src/converts/image-to-canvas.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { consoleWarn } from '../log'
import { resolveOptions } from '../resolve-options'
import { loadMedia } from '../utils'
import { isOnlyAppleWebKit, loadMedia } from '../utils'

import type { Options, ResolvedOptions } from '../options'

Expand All @@ -11,10 +11,19 @@ export async function imageToCanvas<T extends HTMLImageElement>(
const resolved = await resolveOptions(image, options)
const loaded = await loadMedia(image, { timeout: resolved.timeout })
const { canvas, context } = createCanvas(image.ownerDocument, resolved)
try {
context?.drawImage(loaded, 0, 0, canvas.width, canvas.height)
} catch (error) {
consoleWarn('Failed to image to canvas - ', error)
// fix: image not decode when drawImage svg+xml in safari/webkit
const counts = isOnlyAppleWebKit ? (resolved.context.images.size || 1) : 1
for (let i = 0; i < counts; i++) {
await new Promise<void>(resolve => {
setTimeout(() => {
try {
context?.drawImage(loaded, 0, 0, canvas.width, canvas.height)
} catch (error) {
consoleWarn('Failed to image to canvas - ', error)
}
resolve()
}, i)
})
}
return canvas
}
Expand Down
10 changes: 7 additions & 3 deletions src/embed-image-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ export async function embedImageElement<T extends HTMLImageElement | SVGImageEle
clone: T,
options: ResolvedOptions,
) {
if (isImageElement(clone) && !isDataUrl(clone.src)) {
if (isImageElement(clone) && !isDataUrl(clone.currentSrc || clone.src)) {
const originSrc = clone.currentSrc || clone.src
clone.srcset = ''
clone.src = await fetchDataUrl(clone.currentSrc || clone.src, options, true)
clone.dataset.originalSrc = originSrc
clone.src = await fetchDataUrl(originSrc, options, true)
} else if (isSVGElementNode(clone) && !isDataUrl(clone.href.baseVal)) {
clone.href.baseVal = await fetchDataUrl(clone.href.baseVal, options, true)
const originSrc = clone.href.baseVal
clone.dataset.originalSrc = originSrc
clone.href.baseVal = await fetchDataUrl(originSrc, options, true)
} else {
return
}
Expand Down
3 changes: 3 additions & 0 deletions src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export function fetchBase64(url: string, options: ResolvedOptions, isImage?: boo
export async function fetchDataUrl(url: string, options: ResolvedOptions, isImage?: boolean) {
const { base64, contentType } = await fetchBase64(url, options, isImage)
const mimeType = getMimeType(url) ?? contentType
if (isImage) {
options.context.images.add(url)
}
return `data:${ mimeType };base64,${ base64 }`
}

Expand Down
4 changes: 4 additions & 0 deletions src/log.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
const prefix = '[modern-screenshot]'

export function consoleError(...args: any[]) {
return console.error(prefix, ...args)
}

export function consoleWarn(...args: any[]) {
return console.warn(prefix, ...args)
}
Expand Down
1 change: 1 addition & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export interface ResolvedOptions extends Options {
context: {
styleEl: HTMLStyleElement
fontFamilies: Set<string>
images: Set<string>
tasks: Promise<void>[]
}
}
1 change: 1 addition & 0 deletions src/resolve-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export async function resolveOptions(node: Node, userOptions?: Options): Promise
...userOptions,
context: {
fontFamilies: new Set(),
images: new Set(),
styleEl,
tasks: [],
},
Expand Down
64 changes: 40 additions & 24 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { consoleWarn } from './log'
import { consoleError, consoleWarn } from './log'

export const IN_BROWSER = typeof window !== 'undefined'
export const isElementNode = (node: Node): node is Element => node.nodeType === 1 // Node.ELEMENT_NODE
Expand All @@ -17,6 +17,9 @@ export const isSelectElement = (node: Element): node is HTMLSelectElement => nod
export const isSlotElement = (node: Element): node is HTMLSlotElement => node.tagName === 'SLOT'
export const isIFrameElement = (node: Element): node is HTMLIFrameElement => node.tagName === 'IFRAME'

export const ua = IN_BROWSER ? window.navigator?.userAgent : undefined
export const isOnlyAppleWebKit = ua?.includes('AppleWebKit') && !ua?.includes('Chrome')

export function isDataUrl(url: string) {
return url.startsWith('data:')
}
Expand Down Expand Up @@ -70,6 +73,7 @@ export function createImage(url: string, ownerDocument?: Document | null, useCOR
img.crossOrigin = 'anonymous'
}
img.decoding = 'sync'
img.loading = 'eager'
img.src = url
return img
}
Expand All @@ -93,40 +97,52 @@ export function loadMedia(media: any, options?: LoadMediaOptions): Promise<any>
? createImage(media, ownerDocument)
: media

const onResolve = () => resolve(node)

if (timeout) {
setTimeout(() => resolve(node), timeout)
setTimeout(onResolve, timeout)
}

if (isVideoElement(node)) {
if (node.readyState >= 2 || (!node.src && !node.currentSrc)) return resolve(node)

node.addEventListener(
'loadeddata',
() => resolve(node),
{ once: true },
)
if (node.readyState >= 2 || (!node.currentSrc && !node.src)) return onResolve()
node.addEventListener('loadeddata', onResolve, { once: true })
} else {
const onDecode = () => {
if (isImageElement(node) && 'decode' in node) {
node.decode()
.catch((err: DOMException) => {
consoleWarn(
'Failed to decode image, trying to render anyway',
`src: ${ node.dataset.originalSrc || node.currentSrc || node.src }`,
err,
)
})
.finally(() => {
onResolve()
})
} else {
onResolve()
}
}

if (isSVGImageElementNode(node)) {
if (!node.href.baseVal) return resolve(node)
if (!node.href.baseVal) return onResolve()
} else {
if (!node.src && !node.currentSrc) return resolve(node)
if (!node.currentSrc && !node.src) return onResolve()
if (node.complete) return onDecode()
}

node.addEventListener(
'load',
() => {
if (isSVGImageElementNode(node)) {
resolve(node)
} else {
node.decode().catch(consoleWarn).finally(() => resolve(node))
}
},
{ once: true },
)

node.addEventListener('load', onDecode, { once: true })
node.addEventListener(
'error',
() => resolve(node),
(err: any) => {
consoleError(
'Image load failed',
`src: ${ node.dataset.originalSrc || (isSVGImageElementNode(node) ? node.href.baseVal : (node.currentSrc || node.src)) }`,
err,
)
onResolve()
},
{ once: true },
)
}
Expand Down

1 comment on commit f083ddb

@vercel
Copy link

@vercel vercel bot commented on f083ddb Feb 2, 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-qq15725.vercel.app
modern-screenshot.vercel.app
modern-screenshot-git-master-qq15725.vercel.app

Please sign in to comment.