@tldraw/editor 4.5.2 → 4.6.0-canary.4ec045c286e1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +37 -6
- package/dist-cjs/index.js +6 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +7 -5
- package/dist-cjs/lib/TldrawEditor.js.map +3 -3
- package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js +3 -2
- package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +8 -5
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
- package/dist-cjs/lib/config/TLSessionStateSnapshot.js +8 -5
- package/dist-cjs/lib/config/TLSessionStateSnapshot.js.map +2 -2
- package/dist-cjs/lib/config/TLUserPreferences.js +3 -2
- package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
- package/dist-cjs/lib/config/createTLStore.js +1 -0
- package/dist-cjs/lib/config/createTLStore.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +52 -16
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +62 -6
- package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +4 -3
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +5 -0
- package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +3 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
- package/dist-cjs/lib/exports/FontEmbedder.js +9 -8
- package/dist-cjs/lib/exports/FontEmbedder.js.map +2 -2
- package/dist-cjs/lib/exports/StyleEmbedder.js +27 -15
- package/dist-cjs/lib/exports/StyleEmbedder.js.map +3 -3
- package/dist-cjs/lib/exports/domUtils.js +15 -0
- package/dist-cjs/lib/exports/domUtils.js.map +2 -2
- package/dist-cjs/lib/exports/embedMedia.js +15 -12
- package/dist-cjs/lib/exports/embedMedia.js.map +2 -2
- package/dist-cjs/lib/exports/exportToSvg.js +8 -7
- package/dist-cjs/lib/exports/exportToSvg.js.map +2 -2
- package/dist-cjs/lib/exports/getSvgAsImage.js +181 -29
- package/dist-cjs/lib/exports/getSvgAsImage.js.map +3 -3
- package/dist-cjs/lib/exports/getSvgJsx.js +21 -9
- package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
- package/dist-cjs/lib/globals/environment.js +4 -3
- package/dist-cjs/lib/globals/environment.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useDocumentEvents.js +13 -11
- package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js +3 -2
- package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useScreenBounds.js +10 -6
- package/dist-cjs/lib/hooks/useScreenBounds.js.map +2 -2
- package/dist-cjs/lib/hooks/useViewportHeight.js +13 -11
- package/dist-cjs/lib/hooks/useViewportHeight.js.map +3 -3
- package/dist-cjs/lib/license/Watermark.js +10 -0
- package/dist-cjs/lib/license/Watermark.js.map +2 -2
- package/dist-cjs/lib/primitives/Vec.js +35 -22
- package/dist-cjs/lib/primitives/Vec.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Arc2d.js +6 -13
- package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Circle2d.js +31 -2
- package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +9 -0
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/CubicSpline2d.js +9 -0
- package/dist-cjs/lib/primitives/geometry/CubicSpline2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Edge2d.js +32 -18
- package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +12 -0
- package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Polyline2d.js +51 -12
- package/dist-cjs/lib/primitives/geometry/Polyline2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Stadium2d.js +12 -0
- package/dist-cjs/lib/primitives/geometry/Stadium2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/geometry.bench.js +133 -0
- package/dist-cjs/lib/primitives/geometry/geometry.bench.js.map +7 -0
- package/dist-cjs/lib/primitives/intersect.js +16 -15
- package/dist-cjs/lib/primitives/intersect.js.map +2 -2
- package/dist-cjs/lib/primitives/utils.js +0 -1
- package/dist-cjs/lib/primitives/utils.js.map +2 -2
- package/dist-cjs/lib/utils/browserCanvasMaxSize.js +3 -2
- package/dist-cjs/lib/utils/browserCanvasMaxSize.js.map +2 -2
- package/dist-cjs/lib/utils/dom.js +15 -2
- package/dist-cjs/lib/utils/dom.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +37 -6
- package/dist-esm/index.mjs +8 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +7 -5
- package/dist-esm/lib/TldrawEditor.mjs.map +3 -3
- package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs +2 -1
- package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +8 -5
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
- package/dist-esm/lib/config/TLSessionStateSnapshot.mjs +8 -5
- package/dist-esm/lib/config/TLSessionStateSnapshot.mjs.map +2 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs +3 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
- package/dist-esm/lib/config/createTLStore.mjs +1 -0
- package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +53 -17
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +64 -6
- package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +4 -3
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +5 -0
- package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +3 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/exports/FontEmbedder.mjs +9 -8
- package/dist-esm/lib/exports/FontEmbedder.mjs.map +2 -2
- package/dist-esm/lib/exports/StyleEmbedder.mjs +29 -16
- package/dist-esm/lib/exports/StyleEmbedder.mjs.map +3 -3
- package/dist-esm/lib/exports/domUtils.mjs +15 -0
- package/dist-esm/lib/exports/domUtils.mjs.map +2 -2
- package/dist-esm/lib/exports/embedMedia.mjs +16 -13
- package/dist-esm/lib/exports/embedMedia.mjs.map +2 -2
- package/dist-esm/lib/exports/exportToSvg.mjs +8 -7
- package/dist-esm/lib/exports/exportToSvg.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgAsImage.mjs +181 -29
- package/dist-esm/lib/exports/getSvgAsImage.mjs.map +3 -3
- package/dist-esm/lib/exports/getSvgJsx.mjs +21 -9
- package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
- package/dist-esm/lib/globals/environment.mjs +4 -3
- package/dist-esm/lib/globals/environment.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useDocumentEvents.mjs +13 -11
- package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs +3 -2
- package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useScreenBounds.mjs +10 -6
- package/dist-esm/lib/hooks/useScreenBounds.mjs.map +2 -2
- package/dist-esm/lib/hooks/useViewportHeight.mjs +13 -11
- package/dist-esm/lib/hooks/useViewportHeight.mjs.map +3 -3
- package/dist-esm/lib/license/Watermark.mjs +10 -0
- package/dist-esm/lib/license/Watermark.mjs.map +2 -2
- package/dist-esm/lib/primitives/Vec.mjs +35 -22
- package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs +6 -13
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Circle2d.mjs +31 -2
- package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +9 -0
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/CubicSpline2d.mjs +9 -0
- package/dist-esm/lib/primitives/geometry/CubicSpline2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Edge2d.mjs +32 -18
- package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs +13 -1
- package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Polyline2d.mjs +51 -12
- package/dist-esm/lib/primitives/geometry/Polyline2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Stadium2d.mjs +13 -1
- package/dist-esm/lib/primitives/geometry/Stadium2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/geometry.bench.mjs +132 -0
- package/dist-esm/lib/primitives/geometry/geometry.bench.mjs.map +7 -0
- package/dist-esm/lib/primitives/intersect.mjs +17 -16
- package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
- package/dist-esm/lib/primitives/utils.mjs +0 -1
- package/dist-esm/lib/primitives/utils.mjs.map +2 -2
- package/dist-esm/lib/utils/browserCanvasMaxSize.mjs +3 -2
- package/dist-esm/lib/utils/browserCanvasMaxSize.mjs.map +2 -2
- package/dist-esm/lib/utils/dom.mjs +15 -2
- package/dist-esm/lib/utils/dom.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +3 -0
- package/src/lib/TldrawEditor.tsx +7 -5
- package/src/lib/components/default-components/CanvasShapeIndicators.tsx +2 -1
- package/src/lib/components/default-components/DefaultCanvas.tsx +1 -1
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +8 -5
- package/src/lib/config/TLSessionStateSnapshot.ts +8 -5
- package/src/lib/config/TLUserPreferences.ts +3 -2
- package/src/lib/config/createTLStore.ts +3 -0
- package/src/lib/editor/Editor.ts +53 -15
- package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +7 -6
- package/src/lib/editor/managers/FocusManager/FocusManager.ts +10 -7
- package/src/lib/editor/managers/FontManager/FontManager.test.ts +1 -0
- package/src/lib/editor/managers/FontManager/FontManager.ts +4 -3
- package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +16 -0
- package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +7 -2
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +4 -5
- package/src/lib/editor/managers/TextManager/TextManager.ts +2 -2
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +3 -2
- package/src/lib/editor/types/misc-types.ts +8 -2
- package/src/lib/exports/FontEmbedder.ts +10 -9
- package/src/lib/exports/StyleEmbedder.ts +33 -15
- package/src/lib/exports/domUtils.ts +20 -0
- package/src/lib/exports/embedMedia.ts +23 -17
- package/src/lib/exports/exportToSvg.tsx +8 -7
- package/src/lib/exports/getSvgAsImage.ts +292 -32
- package/src/lib/exports/getSvgJsx.test.ts +103 -101
- package/src/lib/exports/getSvgJsx.tsx +33 -10
- package/src/lib/globals/environment.ts +4 -3
- package/src/lib/hooks/useCanvasEvents.ts +2 -3
- package/src/lib/hooks/useDocumentEvents.ts +16 -11
- package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +3 -3
- package/src/lib/hooks/useScreenBounds.ts +10 -6
- package/src/lib/hooks/useViewportHeight.ts +13 -11
- package/src/lib/license/Watermark.tsx +10 -0
- package/src/lib/primitives/Vec.ts +51 -24
- package/src/lib/primitives/geometry/Arc2d.ts +10 -15
- package/src/lib/primitives/geometry/Circle2d.ts +40 -2
- package/src/lib/primitives/geometry/CubicBezier2d.ts +10 -0
- package/src/lib/primitives/geometry/CubicSpline2d.ts +10 -0
- package/src/lib/primitives/geometry/Edge2d.ts +41 -18
- package/src/lib/primitives/geometry/Ellipse2d.ts +14 -1
- package/src/lib/primitives/geometry/Polyline2d.ts +60 -12
- package/src/lib/primitives/geometry/Stadium2d.ts +14 -1
- package/src/lib/primitives/geometry/geometry.bench.ts +179 -0
- package/src/lib/primitives/intersect.ts +27 -27
- package/src/lib/primitives/utils.ts +4 -4
- package/src/lib/test/TestEditor.ts +1 -0
- package/src/lib/utils/browserCanvasMaxSize.ts +4 -2
- package/src/lib/utils/dom.ts +34 -2
- package/src/version.ts +3 -3
|
@@ -2,6 +2,7 @@ import { FileHelpers, Image, PngHelpers, sleep } from '@tldraw/utils'
|
|
|
2
2
|
import { tlenv } from '../globals/environment'
|
|
3
3
|
import { clampToBrowserMaxCanvasSize } from '../utils/browserCanvasMaxSize'
|
|
4
4
|
import { debugFlags } from '../utils/debug-flags'
|
|
5
|
+
import { getGlobalDocument } from '../utils/dom'
|
|
5
6
|
|
|
6
7
|
/** @public */
|
|
7
8
|
export async function getSvgAsImage(
|
|
@@ -14,7 +15,26 @@ export async function getSvgAsImage(
|
|
|
14
15
|
pixelRatio?: number
|
|
15
16
|
}
|
|
16
17
|
) {
|
|
17
|
-
const
|
|
18
|
+
const result = await getSvgAsImageWithOptions(svgString, options)
|
|
19
|
+
return result?.blob ?? null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @internal */
|
|
23
|
+
export async function getSvgAsImageWithOptions(
|
|
24
|
+
svgString: string,
|
|
25
|
+
options: {
|
|
26
|
+
type: 'png' | 'jpeg' | 'webp'
|
|
27
|
+
width: number
|
|
28
|
+
height: number
|
|
29
|
+
quality?: number
|
|
30
|
+
pixelRatio?: number
|
|
31
|
+
trimPadding?: number
|
|
32
|
+
scale?: number
|
|
33
|
+
}
|
|
34
|
+
): Promise<{ blob: Blob; width: number; height: number } | null> {
|
|
35
|
+
const { type, width, height, quality = 1, pixelRatio = 2, trimPadding = 0, scale = 1 } = options
|
|
36
|
+
|
|
37
|
+
if (width <= 0 || height <= 0) return null
|
|
18
38
|
|
|
19
39
|
let [clampedWidth, clampedHeight] = clampToBrowserMaxCanvasSize(
|
|
20
40
|
width * pixelRatio,
|
|
@@ -24,12 +44,58 @@ export async function getSvgAsImage(
|
|
|
24
44
|
clampedHeight = Math.floor(clampedHeight)
|
|
25
45
|
const effectiveScale = clampedWidth / width
|
|
26
46
|
|
|
47
|
+
const canvas = await renderSvgToCanvas(svgString, clampedWidth, clampedHeight)
|
|
48
|
+
|
|
49
|
+
if (!canvas) return null
|
|
50
|
+
|
|
51
|
+
// If we rendered with extra padding to capture visual overflow, trim it now
|
|
52
|
+
const outputCanvas =
|
|
53
|
+
trimPadding > 0
|
|
54
|
+
? trimExtraPadding(canvas, trimPadding * scale * effectiveScale)
|
|
55
|
+
: { canvas, width: clampedWidth, height: clampedHeight }
|
|
56
|
+
|
|
57
|
+
const blob = await new Promise<Blob | null>((resolve) =>
|
|
58
|
+
outputCanvas.canvas.toBlob(
|
|
59
|
+
(blob) => {
|
|
60
|
+
if (!blob || debugFlags.throwToBlob.get()) {
|
|
61
|
+
resolve(null)
|
|
62
|
+
}
|
|
63
|
+
resolve(blob)
|
|
64
|
+
},
|
|
65
|
+
'image/' + type,
|
|
66
|
+
quality
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if (!blob) return null
|
|
71
|
+
|
|
72
|
+
let resultBlob: Blob
|
|
73
|
+
if (type === 'png') {
|
|
74
|
+
resultBlob = PngHelpers.setPhysChunk(new DataView(await blob.arrayBuffer()), effectiveScale, {
|
|
75
|
+
type: 'image/' + type,
|
|
76
|
+
})
|
|
77
|
+
} else {
|
|
78
|
+
resultBlob = blob
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
blob: resultBlob,
|
|
83
|
+
width: outputCanvas.width / effectiveScale,
|
|
84
|
+
height: outputCanvas.height / effectiveScale,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function renderSvgToCanvas(
|
|
89
|
+
svgString: string,
|
|
90
|
+
width: number,
|
|
91
|
+
height: number
|
|
92
|
+
): Promise<HTMLCanvasElement | null> {
|
|
27
93
|
// usually we would use `URL.createObjectURL` here, but chrome has a bug where `blob:` URLs of
|
|
28
94
|
// SVGs that use <foreignObject> mark the canvas as tainted, where data: ones do not.
|
|
29
95
|
// https://issues.chromium.org/issues/41054640
|
|
30
96
|
const svgUrl = await FileHelpers.blobToDataUrl(new Blob([svgString], { type: 'image/svg+xml' }))
|
|
31
97
|
|
|
32
|
-
|
|
98
|
+
return new Promise<HTMLCanvasElement | null>((resolve) => {
|
|
33
99
|
const image = Image()
|
|
34
100
|
image.crossOrigin = 'anonymous'
|
|
35
101
|
|
|
@@ -42,18 +108,13 @@ export async function getSvgAsImage(
|
|
|
42
108
|
await sleep(250)
|
|
43
109
|
}
|
|
44
110
|
|
|
45
|
-
const canvas =
|
|
111
|
+
const canvas = getGlobalDocument().createElement('canvas') as HTMLCanvasElement
|
|
46
112
|
const ctx = canvas.getContext('2d')!
|
|
47
|
-
|
|
48
|
-
canvas.
|
|
49
|
-
canvas.height = clampedHeight
|
|
50
|
-
|
|
113
|
+
canvas.width = width
|
|
114
|
+
canvas.height = height
|
|
51
115
|
ctx.imageSmoothingEnabled = true
|
|
52
116
|
ctx.imageSmoothingQuality = 'high'
|
|
53
|
-
ctx.drawImage(image, 0, 0,
|
|
54
|
-
|
|
55
|
-
URL.revokeObjectURL(svgUrl)
|
|
56
|
-
|
|
117
|
+
ctx.drawImage(image, 0, 0, width, height)
|
|
57
118
|
resolve(canvas)
|
|
58
119
|
}
|
|
59
120
|
|
|
@@ -63,30 +124,229 @@ export async function getSvgAsImage(
|
|
|
63
124
|
|
|
64
125
|
image.src = svgUrl
|
|
65
126
|
})
|
|
127
|
+
}
|
|
66
128
|
|
|
67
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Scans a canvas from each edge inward (up to trimPaddingPx pixels) to find
|
|
131
|
+
* the first row/column containing non-background content. Returns the crop
|
|
132
|
+
* rectangle in canvas pixel coordinates, or null if no trimming is needed.
|
|
133
|
+
*/
|
|
134
|
+
function measureContentBounds(
|
|
135
|
+
canvas: HTMLCanvasElement,
|
|
136
|
+
trimPaddingPx: number
|
|
137
|
+
): { cropLeft: number; cropTop: number; cropRight: number; cropBottom: number } | null {
|
|
138
|
+
const w = canvas.width
|
|
139
|
+
const h = canvas.height
|
|
140
|
+
const ctx = canvas.getContext('2d')!
|
|
68
141
|
|
|
69
|
-
const
|
|
70
|
-
canvas.toBlob(
|
|
71
|
-
(blob) => {
|
|
72
|
-
if (!blob || debugFlags.throwToBlob.get()) {
|
|
73
|
-
resolve(null)
|
|
74
|
-
}
|
|
75
|
-
resolve(blob)
|
|
76
|
-
},
|
|
77
|
-
'image/' + type,
|
|
78
|
-
quality
|
|
79
|
-
)
|
|
80
|
-
)
|
|
142
|
+
const extraPx = Math.ceil(trimPaddingPx)
|
|
81
143
|
|
|
82
|
-
if
|
|
144
|
+
// Nothing to trim if the extra padding is negligible or larger than half the canvas
|
|
145
|
+
// (extraPx * 2 >= w means declaredRight <= declaredLeft, producing zero/negative crop)
|
|
146
|
+
if (extraPx <= 0 || extraPx * 2 >= w || extraPx * 2 >= h) return null
|
|
83
147
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
148
|
+
const imageData = ctx.getImageData(0, 0, w, h)
|
|
149
|
+
const data = imageData.data
|
|
150
|
+
|
|
151
|
+
// Determine how to detect "empty" pixels.
|
|
152
|
+
// Sample the corner pixel to detect the background color.
|
|
153
|
+
const cornerR = data[0]
|
|
154
|
+
const cornerG = data[1]
|
|
155
|
+
const cornerB = data[2]
|
|
156
|
+
const cornerA = data[3]
|
|
157
|
+
const hasTransparentBackground = cornerA === 0
|
|
158
|
+
|
|
159
|
+
function isContentPixel(offset: number): boolean {
|
|
160
|
+
if (hasTransparentBackground) {
|
|
161
|
+
// For transparent background, any non-transparent pixel is content
|
|
162
|
+
return data[offset + 3] > 0
|
|
163
|
+
} else {
|
|
164
|
+
// For opaque background, look for pixels that differ from the background
|
|
165
|
+
const a = data[offset + 3]
|
|
166
|
+
if (a !== cornerA) return true
|
|
167
|
+
const r = data[offset]
|
|
168
|
+
const g = data[offset + 1]
|
|
169
|
+
const b = data[offset + 2]
|
|
170
|
+
return r !== cornerR || g !== cornerG || b !== cornerB
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The declared bounds area (content area without extra padding)
|
|
175
|
+
const declaredLeft = extraPx
|
|
176
|
+
const declaredTop = extraPx
|
|
177
|
+
const declaredRight = w - extraPx
|
|
178
|
+
const declaredBottom = h - extraPx
|
|
179
|
+
|
|
180
|
+
// Scan from top edge inward: find first row with content or declared bounds
|
|
181
|
+
let cropTop = declaredTop
|
|
182
|
+
for (let y = 0; y < declaredTop; y++) {
|
|
183
|
+
let hasContent = false
|
|
184
|
+
for (let x = 0; x < w; x++) {
|
|
185
|
+
if (isContentPixel((y * w + x) * 4)) {
|
|
186
|
+
hasContent = true
|
|
187
|
+
break
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (hasContent) {
|
|
191
|
+
cropTop = y
|
|
192
|
+
break
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Scan from bottom edge inward
|
|
197
|
+
let cropBottom = declaredBottom
|
|
198
|
+
for (let y = h - 1; y >= declaredBottom; y--) {
|
|
199
|
+
let hasContent = false
|
|
200
|
+
for (let x = 0; x < w; x++) {
|
|
201
|
+
if (isContentPixel((y * w + x) * 4)) {
|
|
202
|
+
hasContent = true
|
|
203
|
+
break
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (hasContent) {
|
|
207
|
+
cropBottom = y + 1
|
|
208
|
+
break
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Scan from left edge inward
|
|
213
|
+
let cropLeft = declaredLeft
|
|
214
|
+
for (let x = 0; x < declaredLeft; x++) {
|
|
215
|
+
let hasContent = false
|
|
216
|
+
for (let y = cropTop; y < cropBottom; y++) {
|
|
217
|
+
if (isContentPixel((y * w + x) * 4)) {
|
|
218
|
+
hasContent = true
|
|
219
|
+
break
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (hasContent) {
|
|
223
|
+
cropLeft = x
|
|
224
|
+
break
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Scan from right edge inward
|
|
229
|
+
let cropRight = declaredRight
|
|
230
|
+
for (let x = w - 1; x >= declaredRight; x--) {
|
|
231
|
+
let hasContent = false
|
|
232
|
+
for (let y = cropTop; y < cropBottom; y++) {
|
|
233
|
+
if (isContentPixel((y * w + x) * 4)) {
|
|
234
|
+
hasContent = true
|
|
235
|
+
break
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (hasContent) {
|
|
239
|
+
cropRight = x + 1
|
|
240
|
+
break
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// If no trimming needed (content fills or exceeds the entire render area)
|
|
245
|
+
if (cropLeft === 0 && cropTop === 0 && cropRight === w && cropBottom === h) {
|
|
246
|
+
return null
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { cropLeft, cropTop, cropRight, cropBottom }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Trims extra padding from a canvas by scanning from each edge inward to find
|
|
254
|
+
* non-transparent (or non-background) pixels. Stops at either content pixels or
|
|
255
|
+
* the declared bounds (the area without extra padding).
|
|
256
|
+
*/
|
|
257
|
+
function trimExtraPadding(
|
|
258
|
+
canvas: HTMLCanvasElement,
|
|
259
|
+
trimPaddingPx: number
|
|
260
|
+
): { canvas: HTMLCanvasElement; width: number; height: number } {
|
|
261
|
+
const w = canvas.width
|
|
262
|
+
const h = canvas.height
|
|
263
|
+
|
|
264
|
+
const bounds = measureContentBounds(canvas, trimPaddingPx)
|
|
265
|
+
if (!bounds) return { canvas, width: w, height: h }
|
|
266
|
+
|
|
267
|
+
const { cropLeft, cropTop, cropRight, cropBottom } = bounds
|
|
268
|
+
const cropW = cropRight - cropLeft
|
|
269
|
+
const cropH = cropBottom - cropTop
|
|
270
|
+
|
|
271
|
+
// Create a new cropped canvas
|
|
272
|
+
const croppedCanvas = getGlobalDocument().createElement('canvas')
|
|
273
|
+
croppedCanvas.width = cropW
|
|
274
|
+
croppedCanvas.height = cropH
|
|
275
|
+
const croppedCtx = croppedCanvas.getContext('2d')!
|
|
276
|
+
croppedCtx.drawImage(canvas, cropLeft, cropTop, cropW, cropH, 0, 0, cropW, cropH)
|
|
277
|
+
|
|
278
|
+
return { canvas: croppedCanvas, width: cropW, height: cropH }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Trims an SVG string to its visual content bounds by rendering it to a
|
|
283
|
+
* temporary canvas, measuring the actual content area, then adjusting the
|
|
284
|
+
* SVG's viewBox and dimensions to match.
|
|
285
|
+
*
|
|
286
|
+
* @param svgString - The SVG string to trim.
|
|
287
|
+
* @param options - Options for trimming.
|
|
288
|
+
* @returns The trimmed SVG string with updated dimensions, or null if no trimming was needed.
|
|
289
|
+
*
|
|
290
|
+
* @internal
|
|
291
|
+
*/
|
|
292
|
+
export async function trimSvgToContent(
|
|
293
|
+
svgString: string,
|
|
294
|
+
options: {
|
|
295
|
+
width: number
|
|
296
|
+
height: number
|
|
297
|
+
trimPadding: number
|
|
298
|
+
scale: number
|
|
91
299
|
}
|
|
300
|
+
): Promise<{ svg: string; width: number; height: number } | null> {
|
|
301
|
+
const { width, height, trimPadding, scale } = options
|
|
302
|
+
|
|
303
|
+
if (trimPadding <= 0) return null
|
|
304
|
+
|
|
305
|
+
// Render SVG to a temporary canvas at 1:1 pixel ratio
|
|
306
|
+
const canvasWidth = Math.floor(width)
|
|
307
|
+
const canvasHeight = Math.floor(height)
|
|
308
|
+
|
|
309
|
+
if (canvasWidth <= 0 || canvasHeight <= 0) return null
|
|
310
|
+
|
|
311
|
+
const canvas = await renderSvgToCanvas(svgString, canvasWidth, canvasHeight)
|
|
312
|
+
|
|
313
|
+
if (!canvas) return null
|
|
314
|
+
|
|
315
|
+
// Measure content bounds on the canvas
|
|
316
|
+
const trimPaddingPx = trimPadding * scale
|
|
317
|
+
const bounds = measureContentBounds(canvas, trimPaddingPx)
|
|
318
|
+
if (!bounds) return null
|
|
319
|
+
|
|
320
|
+
const { cropLeft, cropTop, cropRight, cropBottom } = bounds
|
|
321
|
+
|
|
322
|
+
// Parse the SVG to get the current viewBox
|
|
323
|
+
const parser = new DOMParser()
|
|
324
|
+
const doc = parser.parseFromString(svgString, 'image/svg+xml')
|
|
325
|
+
const svgEl = doc.documentElement
|
|
326
|
+
|
|
327
|
+
const viewBoxAttr = svgEl.getAttribute('viewBox')
|
|
328
|
+
if (!viewBoxAttr) return null
|
|
329
|
+
|
|
330
|
+
const [vbMinX, vbMinY, vbW, vbH] = viewBoxAttr.split(/\s+/).map(Number)
|
|
331
|
+
|
|
332
|
+
// Convert canvas pixel coords to viewBox coords
|
|
333
|
+
const newMinX = vbMinX + (cropLeft / canvasWidth) * vbW
|
|
334
|
+
const newMinY = vbMinY + (cropTop / canvasHeight) * vbH
|
|
335
|
+
const newVbW = ((cropRight - cropLeft) / canvasWidth) * vbW
|
|
336
|
+
const newVbH = ((cropBottom - cropTop) / canvasHeight) * vbH
|
|
337
|
+
|
|
338
|
+
// New SVG dimensions maintain the same scale
|
|
339
|
+
const newWidth = newVbW * scale
|
|
340
|
+
const newHeight = newVbH * scale
|
|
341
|
+
|
|
342
|
+
// Update SVG attributes
|
|
343
|
+
svgEl.setAttribute('viewBox', `${newMinX} ${newMinY} ${newVbW} ${newVbH}`)
|
|
344
|
+
svgEl.setAttribute('width', String(newWidth))
|
|
345
|
+
svgEl.setAttribute('height', String(newHeight))
|
|
346
|
+
|
|
347
|
+
// Serialize back
|
|
348
|
+
const serializer = new XMLSerializer()
|
|
349
|
+
const newSvgString = serializer.serializeToString(svgEl)
|
|
350
|
+
|
|
351
|
+
return { svg: newSvgString, width: newWidth, height: newHeight }
|
|
92
352
|
}
|