html-to-image
Version:
Generates an image from a DOM node using HTML5 canvas and SVG.
262 lines (218 loc) • 6.8 kB
text/typescript
import type { Options } from './types'
export function resolveUrl(url: string, baseUrl: string | null): string {
// url is absolute already
if (url.match(/^[a-z]+:\/\//i)) {
return url
}
// url is absolute already, without protocol
if (url.match(/^\/\//)) {
return window.location.protocol + url
}
// dataURI, mailto:, tel:, etc.
if (url.match(/^[a-z]+:/i)) {
return url
}
const doc = document.implementation.createHTMLDocument()
const base = doc.createElement('base')
const a = doc.createElement('a')
doc.head.appendChild(base)
doc.body.appendChild(a)
if (baseUrl) {
base.href = baseUrl
}
a.href = url
return a.href
}
export const uuid = (() => {
// generate uuid for className of pseudo elements.
// We should not use GUIDs, otherwise pseudo elements sometimes cannot be captured.
let counter = 0
// ref: http://stackoverflow.com/a/6248722/2519373
const random = () =>
// eslint-disable-next-line no-bitwise
`0000${((Math.random() * 36 ** 4) << 0).toString(36)}`.slice(-4)
return () => {
counter += 1
return `u${random()}${counter}`
}
})()
export function delay<T>(ms: number) {
return (args: T) =>
new Promise<T>((resolve) => {
setTimeout(() => resolve(args), ms)
})
}
export function toArray<T>(arrayLike: any): T[] {
const arr: T[] = []
for (let i = 0, l = arrayLike.length; i < l; i++) {
arr.push(arrayLike[i])
}
return arr
}
let styleProps: string[] | null = null
export function getStyleProperties(options: Options = {}): string[] {
if (styleProps) {
return styleProps
}
if (options.includeStyleProperties) {
styleProps = options.includeStyleProperties
return styleProps
}
styleProps = toArray(window.getComputedStyle(document.documentElement))
return styleProps
}
function px(node: HTMLElement, styleProperty: string) {
const win = node.ownerDocument.defaultView || window
const val = win.getComputedStyle(node).getPropertyValue(styleProperty)
return val ? parseFloat(val.replace('px', '')) : 0
}
function getNodeWidth(node: HTMLElement) {
const leftBorder = px(node, 'border-left-width')
const rightBorder = px(node, 'border-right-width')
return node.clientWidth + leftBorder + rightBorder
}
function getNodeHeight(node: HTMLElement) {
const topBorder = px(node, 'border-top-width')
const bottomBorder = px(node, 'border-bottom-width')
return node.clientHeight + topBorder + bottomBorder
}
export function getImageSize(targetNode: HTMLElement, options: Options = {}) {
const width = options.width || getNodeWidth(targetNode)
const height = options.height || getNodeHeight(targetNode)
return { width, height }
}
export function getPixelRatio() {
let ratio
let FINAL_PROCESS
try {
FINAL_PROCESS = process
} catch (e) {
// pass
}
const val =
FINAL_PROCESS && FINAL_PROCESS.env
? FINAL_PROCESS.env.devicePixelRatio
: null
if (val) {
ratio = parseInt(val, 10)
if (Number.isNaN(ratio)) {
ratio = 1
}
}
return ratio || window.devicePixelRatio || 1
}
// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
const canvasDimensionLimit = 16384
export function checkCanvasDimensions(canvas: HTMLCanvasElement) {
if (
canvas.width > canvasDimensionLimit ||
canvas.height > canvasDimensionLimit
) {
if (
canvas.width > canvasDimensionLimit &&
canvas.height > canvasDimensionLimit
) {
if (canvas.width > canvas.height) {
canvas.height *= canvasDimensionLimit / canvas.width
canvas.width = canvasDimensionLimit
} else {
canvas.width *= canvasDimensionLimit / canvas.height
canvas.height = canvasDimensionLimit
}
} else if (canvas.width > canvasDimensionLimit) {
canvas.height *= canvasDimensionLimit / canvas.width
canvas.width = canvasDimensionLimit
} else {
canvas.width *= canvasDimensionLimit / canvas.height
canvas.height = canvasDimensionLimit
}
}
}
export function canvasToBlob(
canvas: HTMLCanvasElement,
options: Options = {},
): Promise<Blob | null> {
if (canvas.toBlob) {
return new Promise((resolve) => {
canvas.toBlob(
resolve,
options.type ? options.type : 'image/png',
options.quality ? options.quality : 1,
)
})
}
return new Promise((resolve) => {
const binaryString = window.atob(
canvas
.toDataURL(
options.type ? options.type : undefined,
options.quality ? options.quality : undefined,
)
.split(',')[1],
)
const len = binaryString.length
const binaryArray = new Uint8Array(len)
for (let i = 0; i < len; i += 1) {
binaryArray[i] = binaryString.charCodeAt(i)
}
resolve(
new Blob([binaryArray], {
type: options.type ? options.type : 'image/png',
}),
)
})
}
export function createImage(url: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => {
img.decode().then(() => {
requestAnimationFrame(() => resolve(img))
})
}
img.onerror = reject
img.crossOrigin = 'anonymous'
img.decoding = 'async'
img.src = url
})
}
export async function svgToDataURL(svg: SVGElement): Promise<string> {
return Promise.resolve()
.then(() => new XMLSerializer().serializeToString(svg))
.then(encodeURIComponent)
.then((html) => `data:image/svg+xml;charset=utf-8,${html}`)
}
export async function nodeToDataURL(
node: HTMLElement,
width: number,
height: number,
): Promise<string> {
const xmlns = 'http://www.w3.org/2000/svg'
const svg = document.createElementNS(xmlns, 'svg')
const foreignObject = document.createElementNS(xmlns, 'foreignObject')
svg.setAttribute('width', `${width}`)
svg.setAttribute('height', `${height}`)
svg.setAttribute('viewBox', `0 0 ${width} ${height}`)
foreignObject.setAttribute('width', '100%')
foreignObject.setAttribute('height', '100%')
foreignObject.setAttribute('x', '0')
foreignObject.setAttribute('y', '0')
foreignObject.setAttribute('externalResourcesRequired', 'true')
svg.appendChild(foreignObject)
foreignObject.appendChild(node)
return svgToDataURL(svg)
}
export const isInstanceOfElement = <
T extends typeof Element | typeof HTMLElement | typeof SVGImageElement,
>(
node: Element | HTMLElement | SVGImageElement,
instance: T,
): node is T['prototype'] => {
if (node instanceof instance) return true
const nodePrototype = Object.getPrototypeOf(node)
if (nodePrototype === null) return false
return (
nodePrototype.constructor.name === instance.name ||
isInstanceOfElement(nodePrototype, instance)
)
}