diff --git a/src/browser.ts b/src/browser.ts new file mode 100644 index 00000000..dde4b3c6 --- /dev/null +++ b/src/browser.ts @@ -0,0 +1,7 @@ +export const isSafari = () => { + const platform = navigator.platform.toLowerCase() + if (platform.includes('linux') || platform.includes('win')) { + return false + } + return /^((?!chrome|android).)*safari/i.test(navigator.userAgent) +} diff --git a/src/embed-images.ts b/src/embed-images.ts index 0d7b51ad..c5d00b71 100644 --- a/src/embed-images.ts +++ b/src/embed-images.ts @@ -1,6 +1,6 @@ import { Options } from './types' import { embedResources } from './embed-resources' -import { toArray, isInstanceOfElement } from './util' +import { toArray, isInstanceOfElement, setReCanvasDrawCount } from './util' import { isDataUrl, resourceToDataURL } from './dataurl' import { getMimeType } from './mimes' @@ -67,7 +67,14 @@ async function embedImageNode( const image = clonedNode as HTMLImageElement if (image.decode) { - image.decode = resolve as any + image.onload = () => { + image.decode().finally(() => { + requestAnimationFrame(resolve) + // fix safari blank image bug + setReCanvasDrawCount(options) + }) + } + image.decoding = 'sync' } if (image.loading === 'lazy') { diff --git a/src/index.ts b/src/index.ts index 2de59a30..8eecf579 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import { canvasToBlob, nodeToDataURL, checkCanvasDimensions, + reCanvasDraw, } from './util' export async function toSvg( @@ -54,6 +55,8 @@ export async function toCanvas( } context.drawImage(img, 0, 0, canvas.width, canvas.height) + // fix safari blank image bug + await reCanvasDraw(options, img, canvas, context) return canvas } diff --git a/src/types.ts b/src/types.ts index 6023c3c2..e625b27c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -102,3 +102,7 @@ export interface Options { */ onImageErrorHandler?: OnErrorEventHandler } + +export interface PrivateOptions extends Options { + reCanvasDrawCount?: number +} diff --git a/src/util.ts b/src/util.ts index 3d430c8f..df751052 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,4 +1,5 @@ -import type { Options } from './types' +import { isSafari } from './browser' +import type { Options, PrivateOptions } from './types' export function resolveUrl(url: string, baseUrl: string | null): string { // url is absolute already @@ -200,13 +201,13 @@ export function createImage(url: string): Promise { return new Promise((resolve, reject) => { const img = new Image() img.onload = () => { - img.decode().then(() => { + img.decode().finally(() => { requestAnimationFrame(() => resolve(img)) }) } img.onerror = reject img.crossOrigin = 'anonymous' - img.decoding = 'async' + img.decoding = 'sync' img.src = url }) } @@ -259,3 +260,32 @@ export const isInstanceOfElement = < isInstanceOfElement(nodePrototype, instance) ) } + +export const setReCanvasDrawCount = (option: PrivateOptions) => { + if (!isSafari()) return + option.reCanvasDrawCount ??= 0 + option.reCanvasDrawCount += 1 +} + +export const reCanvasDraw = async ( + option: PrivateOptions, + img: HTMLImageElement, + canvas: HTMLCanvasElement, + ctx: CanvasRenderingContext2D, +) => { + if (!isSafari()) return + + for (let i = 0; i < (option.reCanvasDrawCount ?? 0); i++) { + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(() => { + // safari preloading + ctx.clearRect(0, 0, canvas.width, canvas.height) + ctx.drawImage(img, 0, 0, canvas.width, canvas.height) + resolve() + }, 100) + }) + } + + Reflect.deleteProperty(option, 'reCanvasDrawCount') +}