UNPKG

@tldraw/editor

Version:

tldraw infinite canvas SDK (editor).

8 lines (7 loc) • 16.2 kB
{ "version": 3, "sources": ["../../../src/lib/exports/getSvgAsImage.ts"], "sourcesContent": ["import { FileHelpers, Image, PngHelpers, sleep } from '@tldraw/utils'\nimport { tlenv } from '../globals/environment'\nimport { clampToBrowserMaxCanvasSize } from '../utils/browserCanvasMaxSize'\nimport { debugFlags } from '../utils/debug-flags'\nimport { getGlobalDocument } from '../utils/dom'\n\n/** @public */\nexport async function getSvgAsImage(\n\tsvgString: string,\n\toptions: {\n\t\ttype: 'png' | 'jpeg' | 'webp'\n\t\twidth: number\n\t\theight: number\n\t\tquality?: number\n\t\tpixelRatio?: number\n\t}\n) {\n\tconst result = await getSvgAsImageWithOptions(svgString, options)\n\treturn result?.blob ?? null\n}\n\n/** @internal */\nexport async function getSvgAsImageWithOptions(\n\tsvgString: string,\n\toptions: {\n\t\ttype: 'png' | 'jpeg' | 'webp'\n\t\twidth: number\n\t\theight: number\n\t\tquality?: number\n\t\tpixelRatio?: number\n\t\ttrimPadding?: number\n\t\tscale?: number\n\t}\n): Promise<{ blob: Blob; width: number; height: number } | null> {\n\tconst { type, width, height, quality = 1, pixelRatio = 2, trimPadding = 0, scale = 1 } = options\n\n\tif (width <= 0 || height <= 0) return null\n\n\tlet [clampedWidth, clampedHeight] = clampToBrowserMaxCanvasSize(\n\t\twidth * pixelRatio,\n\t\theight * pixelRatio\n\t)\n\tclampedWidth = Math.floor(clampedWidth)\n\tclampedHeight = Math.floor(clampedHeight)\n\tconst effectiveScale = clampedWidth / width\n\n\tconst canvas = await renderSvgToCanvas(svgString, clampedWidth, clampedHeight)\n\n\tif (!canvas) return null\n\n\t// If we rendered with extra padding to capture visual overflow, trim it now\n\tconst outputCanvas =\n\t\ttrimPadding > 0\n\t\t\t? trimExtraPadding(canvas, trimPadding * scale * effectiveScale)\n\t\t\t: { canvas, width: clampedWidth, height: clampedHeight }\n\n\tconst blob = await new Promise<Blob | null>((resolve) =>\n\t\toutputCanvas.canvas.toBlob(\n\t\t\t(blob) => {\n\t\t\t\tif (!blob || debugFlags.throwToBlob.get()) {\n\t\t\t\t\tresolve(null)\n\t\t\t\t}\n\t\t\t\tresolve(blob)\n\t\t\t},\n\t\t\t'image/' + type,\n\t\t\tquality\n\t\t)\n\t)\n\n\tif (!blob) return null\n\n\tlet resultBlob: Blob\n\tif (type === 'png') {\n\t\tresultBlob = PngHelpers.setPhysChunk(new DataView(await blob.arrayBuffer()), effectiveScale, {\n\t\t\ttype: 'image/' + type,\n\t\t})\n\t} else {\n\t\tresultBlob = blob\n\t}\n\n\treturn {\n\t\tblob: resultBlob,\n\t\twidth: outputCanvas.width / effectiveScale,\n\t\theight: outputCanvas.height / effectiveScale,\n\t}\n}\n\nasync function renderSvgToCanvas(\n\tsvgString: string,\n\twidth: number,\n\theight: number\n): Promise<HTMLCanvasElement | null> {\n\t// usually we would use `URL.createObjectURL` here, but chrome has a bug where `blob:` URLs of\n\t// SVGs that use <foreignObject> mark the canvas as tainted, where data: ones do not.\n\t// https://issues.chromium.org/issues/41054640\n\tconst svgUrl = await FileHelpers.blobToDataUrl(new Blob([svgString], { type: 'image/svg+xml' }))\n\n\treturn new Promise<HTMLCanvasElement | null>((resolve) => {\n\t\tconst image = Image()\n\t\timage.crossOrigin = 'anonymous'\n\n\t\timage.onload = async () => {\n\t\t\t// safari will fire `onLoad` before the fonts in the SVG are\n\t\t\t// actually loaded. just waiting around a while is brittle, but\n\t\t\t// there doesn't seem to be any better solution for now :( see\n\t\t\t// https://bugs.webkit.org/show_bug.cgi?id=219770\n\t\t\tif (tlenv.isSafari) {\n\t\t\t\tawait sleep(250)\n\t\t\t}\n\n\t\t\tconst canvas = getGlobalDocument().createElement('canvas') as HTMLCanvasElement\n\t\t\tconst ctx = canvas.getContext('2d')!\n\t\t\tcanvas.width = width\n\t\t\tcanvas.height = height\n\t\t\tctx.imageSmoothingEnabled = true\n\t\t\tctx.imageSmoothingQuality = 'high'\n\t\t\tctx.drawImage(image, 0, 0, width, height)\n\t\t\tresolve(canvas)\n\t\t}\n\n\t\timage.onerror = () => {\n\t\t\tresolve(null)\n\t\t}\n\n\t\timage.src = svgUrl\n\t})\n}\n\n/**\n * Scans a canvas from each edge inward (up to trimPaddingPx pixels) to find\n * the first row/column containing non-background content. Returns the crop\n * rectangle in canvas pixel coordinates, or null if no trimming is needed.\n */\nfunction measureContentBounds(\n\tcanvas: HTMLCanvasElement,\n\ttrimPaddingPx: number\n): { cropLeft: number; cropTop: number; cropRight: number; cropBottom: number } | null {\n\tconst w = canvas.width\n\tconst h = canvas.height\n\tconst ctx = canvas.getContext('2d')!\n\n\tconst extraPx = Math.ceil(trimPaddingPx)\n\n\t// Nothing to trim if the extra padding is negligible or larger than half the canvas\n\t// (extraPx * 2 >= w means declaredRight <= declaredLeft, producing zero/negative crop)\n\tif (extraPx <= 0 || extraPx * 2 >= w || extraPx * 2 >= h) return null\n\n\tconst imageData = ctx.getImageData(0, 0, w, h)\n\tconst data = imageData.data\n\n\t// Determine how to detect \"empty\" pixels.\n\t// Sample the corner pixel to detect the background color.\n\tconst cornerR = data[0]\n\tconst cornerG = data[1]\n\tconst cornerB = data[2]\n\tconst cornerA = data[3]\n\tconst hasTransparentBackground = cornerA === 0\n\n\tfunction isContentPixel(offset: number): boolean {\n\t\tif (hasTransparentBackground) {\n\t\t\t// For transparent background, any non-transparent pixel is content\n\t\t\treturn data[offset + 3] > 0\n\t\t} else {\n\t\t\t// For opaque background, look for pixels that differ from the background\n\t\t\tconst a = data[offset + 3]\n\t\t\tif (a !== cornerA) return true\n\t\t\tconst r = data[offset]\n\t\t\tconst g = data[offset + 1]\n\t\t\tconst b = data[offset + 2]\n\t\t\treturn r !== cornerR || g !== cornerG || b !== cornerB\n\t\t}\n\t}\n\n\t// The declared bounds area (content area without extra padding)\n\tconst declaredLeft = extraPx\n\tconst declaredTop = extraPx\n\tconst declaredRight = w - extraPx\n\tconst declaredBottom = h - extraPx\n\n\t// Scan from top edge inward: find first row with content or declared bounds\n\tlet cropTop = declaredTop\n\tfor (let y = 0; y < declaredTop; y++) {\n\t\tlet hasContent = false\n\t\tfor (let x = 0; x < w; x++) {\n\t\t\tif (isContentPixel((y * w + x) * 4)) {\n\t\t\t\thasContent = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif (hasContent) {\n\t\t\tcropTop = y\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Scan from bottom edge inward\n\tlet cropBottom = declaredBottom\n\tfor (let y = h - 1; y >= declaredBottom; y--) {\n\t\tlet hasContent = false\n\t\tfor (let x = 0; x < w; x++) {\n\t\t\tif (isContentPixel((y * w + x) * 4)) {\n\t\t\t\thasContent = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif (hasContent) {\n\t\t\tcropBottom = y + 1\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Scan from left edge inward\n\tlet cropLeft = declaredLeft\n\tfor (let x = 0; x < declaredLeft; x++) {\n\t\tlet hasContent = false\n\t\tfor (let y = cropTop; y < cropBottom; y++) {\n\t\t\tif (isContentPixel((y * w + x) * 4)) {\n\t\t\t\thasContent = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif (hasContent) {\n\t\t\tcropLeft = x\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Scan from right edge inward\n\tlet cropRight = declaredRight\n\tfor (let x = w - 1; x >= declaredRight; x--) {\n\t\tlet hasContent = false\n\t\tfor (let y = cropTop; y < cropBottom; y++) {\n\t\t\tif (isContentPixel((y * w + x) * 4)) {\n\t\t\t\thasContent = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif (hasContent) {\n\t\t\tcropRight = x + 1\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// If no trimming needed (content fills or exceeds the entire render area)\n\tif (cropLeft === 0 && cropTop === 0 && cropRight === w && cropBottom === h) {\n\t\treturn null\n\t}\n\n\treturn { cropLeft, cropTop, cropRight, cropBottom }\n}\n\n/**\n * Trims extra padding from a canvas by scanning from each edge inward to find\n * non-transparent (or non-background) pixels. Stops at either content pixels or\n * the declared bounds (the area without extra padding).\n */\nfunction trimExtraPadding(\n\tcanvas: HTMLCanvasElement,\n\ttrimPaddingPx: number\n): { canvas: HTMLCanvasElement; width: number; height: number } {\n\tconst w = canvas.width\n\tconst h = canvas.height\n\n\tconst bounds = measureContentBounds(canvas, trimPaddingPx)\n\tif (!bounds) return { canvas, width: w, height: h }\n\n\tconst { cropLeft, cropTop, cropRight, cropBottom } = bounds\n\tconst cropW = cropRight - cropLeft\n\tconst cropH = cropBottom - cropTop\n\n\t// Create a new cropped canvas\n\tconst croppedCanvas = getGlobalDocument().createElement('canvas')\n\tcroppedCanvas.width = cropW\n\tcroppedCanvas.height = cropH\n\tconst croppedCtx = croppedCanvas.getContext('2d')!\n\tcroppedCtx.drawImage(canvas, cropLeft, cropTop, cropW, cropH, 0, 0, cropW, cropH)\n\n\treturn { canvas: croppedCanvas, width: cropW, height: cropH }\n}\n\n/**\n * Trims an SVG string to its visual content bounds by rendering it to a\n * temporary canvas, measuring the actual content area, then adjusting the\n * SVG's viewBox and dimensions to match.\n *\n * @param svgString - The SVG string to trim.\n * @param options - Options for trimming.\n * @returns The trimmed SVG string with updated dimensions, or null if no trimming was needed.\n *\n * @internal\n */\nexport async function trimSvgToContent(\n\tsvgString: string,\n\toptions: {\n\t\twidth: number\n\t\theight: number\n\t\ttrimPadding: number\n\t\tscale: number\n\t}\n): Promise<{ svg: string; width: number; height: number } | null> {\n\tconst { width, height, trimPadding, scale } = options\n\n\tif (trimPadding <= 0) return null\n\n\t// Render SVG to a temporary canvas at 1:1 pixel ratio\n\tconst canvasWidth = Math.floor(width)\n\tconst canvasHeight = Math.floor(height)\n\n\tif (canvasWidth <= 0 || canvasHeight <= 0) return null\n\n\tconst canvas = await renderSvgToCanvas(svgString, canvasWidth, canvasHeight)\n\n\tif (!canvas) return null\n\n\t// Measure content bounds on the canvas\n\tconst trimPaddingPx = trimPadding * scale\n\tconst bounds = measureContentBounds(canvas, trimPaddingPx)\n\tif (!bounds) return null\n\n\tconst { cropLeft, cropTop, cropRight, cropBottom } = bounds\n\n\t// Parse the SVG to get the current viewBox\n\tconst parser = new DOMParser()\n\tconst doc = parser.parseFromString(svgString, 'image/svg+xml')\n\tconst svgEl = doc.documentElement\n\n\tconst viewBoxAttr = svgEl.getAttribute('viewBox')\n\tif (!viewBoxAttr) return null\n\n\tconst [vbMinX, vbMinY, vbW, vbH] = viewBoxAttr.split(/\\s+/).map(Number)\n\n\t// Convert canvas pixel coords to viewBox coords\n\tconst newMinX = vbMinX + (cropLeft / canvasWidth) * vbW\n\tconst newMinY = vbMinY + (cropTop / canvasHeight) * vbH\n\tconst newVbW = ((cropRight - cropLeft) / canvasWidth) * vbW\n\tconst newVbH = ((cropBottom - cropTop) / canvasHeight) * vbH\n\n\t// New SVG dimensions maintain the same scale\n\tconst newWidth = newVbW * scale\n\tconst newHeight = newVbH * scale\n\n\t// Update SVG attributes\n\tsvgEl.setAttribute('viewBox', `${newMinX} ${newMinY} ${newVbW} ${newVbH}`)\n\tsvgEl.setAttribute('width', String(newWidth))\n\tsvgEl.setAttribute('height', String(newHeight))\n\n\t// Serialize back\n\tconst serializer = new XMLSerializer()\n\tconst newSvgString = serializer.serializeToString(svgEl)\n\n\treturn { svg: newSvgString, width: newWidth, height: newHeight }\n}\n"], "mappings": "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", "names": ["blob"] }