Tldraw/packages/editor/src/lib/utils/export.ts

161 wiersze
4.1 KiB
TypeScript

import { TLGeoShape, TLNoteShape, TLShape } from '@tldraw/tlschema'
import { debugFlags } from './debug-flags'
import { setPhysChunk } from './png'
/** @public */
export type TLCopyType = 'svg' | 'png' | 'jpeg' | 'json'
/** @public */
export type TLExportType = 'svg' | 'png' | 'jpeg' | 'webp' | 'json'
/** @public */
export function getSvgAsString(svg: SVGElement) {
const clone = svg.cloneNode(true) as SVGGraphicsElement
svg.setAttribute('width', +svg.getAttribute('width')! + '')
svg.setAttribute('height', +svg.getAttribute('height')! + '')
const out = new XMLSerializer()
.serializeToString(clone)
.replaceAll('
 ', '')
.replaceAll(/((\s|")[0-9]*\.[0-9]{2})([0-9]*)(\b|"|\))/g, '$1')
return out
}
/** @public */
export async function getSvgAsImage(
svg: SVGElement,
options: {
type: TLCopyType | TLExportType
quality: number
scale: number
}
) {
const { type, quality, scale } = options
const width = +svg.getAttribute('width')!
const height = +svg.getAttribute('height')!
const dataUrl = await getSvgAsDataUrl(svg)
const canvas = await new Promise<HTMLCanvasElement | null>((resolve) => {
const image = new Image()
image.crossOrigin = 'anonymous'
image.onload = async () => {
// safari will fire `onLoad` before the fonts in the SVG are
// actually loaded. just waiting around a while is brittle, but
// there doesn't seem to be any better solution for now :( see
// https://bugs.webkit.org/show_bug.cgi?id=219770
await new Promise((resolve) => setTimeout(resolve, 250))
const canvas = document.createElement('canvas') as HTMLCanvasElement
const ctx = canvas.getContext('2d')!
canvas.width = width * scale
canvas.height = height * scale
ctx.imageSmoothingEnabled = true
ctx.imageSmoothingQuality = 'high'
ctx.drawImage(image, 0, 0, width * scale, height * scale)
URL.revokeObjectURL(dataUrl)
resolve(canvas)
}
image.onerror = () => {
resolve(null)
}
image.src = dataUrl
})
if (!canvas) return null
const blob = await new Promise<Blob | null>((resolve) =>
canvas.toBlob(
(blob) => {
if (!blob || debugFlags.throwToBlob.value) {
resolve(null)
}
resolve(blob)
},
'image/' + type,
quality
)
)
if (!blob) return null
const view = new DataView(await blob.arrayBuffer())
return setPhysChunk(view, scale, {
type: 'image/' + type,
})
}
/** @public */
export async function getSvgAsDataUrl(svg: SVGElement) {
const clone = svg.cloneNode(true) as SVGGraphicsElement
clone.setAttribute('encoding', 'UTF-8"')
const fileReader = new FileReader()
const imgs = Array.from(clone.querySelectorAll('image')) as SVGImageElement[]
for (const img of imgs) {
const src = img.getAttribute('xlink:href')
if (src) {
if (!src.startsWith('data:')) {
const blob = await (await fetch(src)).blob()
const base64 = await new Promise<string>((resolve, reject) => {
fileReader.onload = () => resolve(fileReader.result as string)
fileReader.onerror = () => reject(fileReader.error)
fileReader.readAsDataURL(blob)
})
img.setAttribute('xlink:href', base64)
}
}
}
return getSvgAsDataUrlSync(clone)
}
/** @public */
export function getSvgAsDataUrlSync(node: SVGElement) {
const svgStr = new XMLSerializer().serializeToString(node)
// NOTE: `unescape` works everywhere although deprecated
const base64SVG = window.btoa(unescape(encodeURIComponent(svgStr)))
return `data:image/svg+xml;base64,${base64SVG}`
}
/** @public */
export function downloadDataURLAsFile(dataUrl: string, filename: string) {
const link = document.createElement('a')
link.href = dataUrl
link.download = filename
link.click()
}
/** @public */
export function getTextBoundingBox(text: SVGTextElement) {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
svg.appendChild(text)
document.body.appendChild(svg)
const bbox = text.getBoundingClientRect()
document.body.removeChild(svg)
return bbox
}
/** @public */
export function isGeoShape(shape: TLShape): shape is TLGeoShape {
return shape.type === 'geo'
}
/** @public */
export function isNoteShape(shape: TLShape): shape is TLNoteShape {
return shape.type === 'note'
}