Skip to content

Commit 2697207

Browse files
committed
Fix complex canvas render on Safari. See #263
1 parent 373ffb6 commit 2697207

2 files changed

Lines changed: 91 additions & 38 deletions

File tree

src/api/snapdom.js

Lines changed: 48 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { captureDOM } from '../core/capture'
33
import { extendIconFonts } from '../modules/iconFonts.js'
44
import { createContext } from '../core/context'
5-
import { toImg, toSvg} from '../exporters/toImg.js'
5+
import { toImg, toSvg } from '../exporters/toImg.js'
66
import { toCanvas } from '../exporters/toCanvas.js'
77
import { toBlob } from '../exporters/toBlob.js'
88
import { rasterize } from '../modules/rasterize.js'
@@ -39,16 +39,16 @@ export async function snapdom(element, userOptions) {
3939
const context = createContext(userOptions)
4040

4141
if (isSafari() && (context.embedFonts === true || hasBackgroundOrMask(element))) {
42-
for (let i = 0; i < 3; i++) {
43-
try {
44-
await safariWarmup(element, userOptions)
45-
console.log('Iteración número:', i)
46-
_safariWarmup = false
47-
} catch {
48-
// swallow error
42+
for (let i = 0; i < 3; i++) {
43+
try {
44+
await safariWarmup(element, userOptions)
45+
console.log('safariWarmup:', i)
46+
_safariWarmup = false
47+
} catch {
48+
// swallow error
49+
}
4950
}
5051
}
51-
}
5252

5353
/* c8 ignore next 1 */
5454
if (context.iconFonts && context.iconFonts.length > 0) extendIconFonts(context.iconFonts)
@@ -193,6 +193,7 @@ snapdom.download = (el, options) => snapdom(el, options).then(result => result.d
193193
*/
194194
async function safariWarmup(element, baseOptions) {
195195
if (_safariWarmup) return
196+
196197
const preflight = {
197198
...baseOptions,
198199
fast: true,
@@ -204,35 +205,46 @@ async function safariWarmup(element, baseOptions) {
204205
try {
205206
url = await captureDOM(element, preflight)
206207
} catch {
207-
// Even if captureDOM fails here, don’t block the real capture.
208-
return
208+
// no bloquea la captura real
209209
}
210210

211-
await new Promise((resolve) => {
212-
// Build offscreen <img> to force decoding
213-
const img = new Image()
214-
img.decoding = 'sync'
215-
img.loading = 'eager'
216-
img.style.position ='fixed'
217-
img.style.left = 0
218-
img.style.top = 0
219-
img.style.width = '10px'
220-
img.style.height = '10px'
221-
img.style.opacity = '0.01'
222-
img.style.transform = 'translateZ(10px)'
223-
img.style.willChange = 'transform,opacity;'
224-
img.src = url
225-
226-
const cleanup = async () => {
227-
await new Promise(r => setTimeout(r, 100))
228-
if (img && img.parentNode) img.parentNode.removeChild(img)
229-
_safariWarmup = true
230-
resolve()
231-
}
211+
// 1) estabiliza layout/paint en WebKit
212+
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)))
213+
214+
if (url) {
215+
await new Promise((resolve) => {
216+
const img = new Image()
217+
try { img.decoding = 'sync'; img.loading = 'eager' } catch { }
218+
img.style.cssText =
219+
'position:fixed;left:0px;top:0px;width:10px;height:10px;opacity:0.01;pointer-events:none;'
220+
img.src = url
221+
document.body.appendChild(img)
222+
223+
; (async () => {
224+
try { if (typeof img.decode === 'function') await img.decode() } catch { }
232225

233-
document.body.appendChild(img)
234-
cleanup()
226+
const start = performance.now()
227+
while (!(img.complete && img.naturalWidth > 0) && performance.now() - start < 900) {
228+
await new Promise(r => setTimeout(r, 50))
229+
}
230+
231+
await new Promise(r => requestAnimationFrame(r))
232+
try { img.remove() } catch { }
233+
resolve()
234+
})()
235+
})
236+
237+
}
238+
239+
// 3) “poke” a los canvas del elemento (Chart.js, etc.)
240+
element.querySelectorAll('canvas').forEach(c => {
241+
try {
242+
const ctx = c.getContext('2d', { willReadFrequently: true })
243+
if (ctx) { ctx.getImageData(0, 0, 1, 1) }
244+
} catch { }
235245
})
246+
247+
_safariWarmup = true
236248
}
237249

238250
/**
@@ -248,9 +260,10 @@ function hasBackgroundOrMask(el) {
248260

249261
const bg = cs.backgroundImage && cs.backgroundImage !== 'none'
250262
const mask = (cs.maskImage && cs.maskImage !== 'none') ||
251-
(cs.webkitMaskImage && cs.webkitMaskImage !== 'none')
263+
(cs.webkitMaskImage && cs.webkitMaskImage !== 'none')
252264

253265
if (bg || mask) return true
266+
if (node.tagName === 'CANVAS') return true
254267
}
255268
return false
256269
}

src/core/clone.js

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -465,16 +465,56 @@ export async function deepClone(node, sessionCache, options) {
465465
clone2.appendChild(placeholder)
466466
return clone2
467467
}
468-
if (node.tagName === 'CANVAS') {
469-
const dataURL = node.toDataURL()
468+
if (node.tagName === 'CANVAS') {
469+
// Safari-safe snapshot: poke + rAF + retry + scratch fallback
470+
let url = ''
471+
try {
472+
const ctx = node.getContext('2d', { willReadFrequently: true })
473+
try { ctx && ctx.getImageData(0, 0, 1, 1) } catch {}
474+
await new Promise(r => requestAnimationFrame(r)) // deja materializar el frame
475+
476+
url = node.toDataURL('image/png')
477+
478+
if (!url || url === 'data:,') {
479+
// reintento rápido
480+
try { ctx && ctx.getImageData(0, 0, 1, 1) } catch {}
481+
await new Promise(r => requestAnimationFrame(r))
482+
url = node.toDataURL('image/png')
483+
484+
// último recurso: copiar a un scratch-canvas y leer desde ahí
485+
if (!url || url === 'data:,') {
486+
const scratch = document.createElement('canvas')
487+
scratch.width = node.width
488+
scratch.height = node.height
489+
const sctx = scratch.getContext('2d')
490+
if (sctx) {
491+
sctx.drawImage(node, 0, 0)
492+
url = scratch.toDataURL('image/png')
493+
}
494+
}
495+
}
496+
} catch {}
497+
470498
const img = document.createElement('img')
471-
img.src = dataURL
499+
try { img.decoding = 'sync'; img.loading = 'eager' } catch {}
500+
if (url) img.src = url
501+
502+
// conservar dimensiones intrínsecas del bitmap
472503
img.width = node.width
473504
img.height = node.height
505+
506+
// conservar caja CSS para no romper layout
507+
try {
508+
const cs = getComputedStyle(node)
509+
if (cs.width) img.style.width = cs.width
510+
if (cs.height) img.style.height = cs.height
511+
} catch {}
512+
474513
sessionCache.nodeMap.set(img, node)
475514
inlineAllStyles(node, img, sessionCache, options)
476515
return img
477516
}
517+
478518
let clone
479519
try {
480520
clone = node.cloneNode(false)

0 commit comments

Comments
 (0)