@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.
Files changed (226) hide show
  1. package/dist-cjs/index.d.ts +37 -6
  2. package/dist-cjs/index.js +6 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +7 -5
  5. package/dist-cjs/lib/TldrawEditor.js.map +3 -3
  6. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js +3 -2
  7. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -1
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +8 -5
  11. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
  12. package/dist-cjs/lib/config/TLSessionStateSnapshot.js +8 -5
  13. package/dist-cjs/lib/config/TLSessionStateSnapshot.js.map +2 -2
  14. package/dist-cjs/lib/config/TLUserPreferences.js +3 -2
  15. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  16. package/dist-cjs/lib/config/createTLStore.js +1 -0
  17. package/dist-cjs/lib/config/createTLStore.js.map +2 -2
  18. package/dist-cjs/lib/editor/Editor.js +52 -16
  19. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  20. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +62 -6
  21. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
  22. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +4 -3
  23. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
  24. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +5 -0
  25. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +2 -2
  26. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +2 -2
  27. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  28. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +3 -2
  29. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  30. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  31. package/dist-cjs/lib/exports/FontEmbedder.js +9 -8
  32. package/dist-cjs/lib/exports/FontEmbedder.js.map +2 -2
  33. package/dist-cjs/lib/exports/StyleEmbedder.js +27 -15
  34. package/dist-cjs/lib/exports/StyleEmbedder.js.map +3 -3
  35. package/dist-cjs/lib/exports/domUtils.js +15 -0
  36. package/dist-cjs/lib/exports/domUtils.js.map +2 -2
  37. package/dist-cjs/lib/exports/embedMedia.js +15 -12
  38. package/dist-cjs/lib/exports/embedMedia.js.map +2 -2
  39. package/dist-cjs/lib/exports/exportToSvg.js +8 -7
  40. package/dist-cjs/lib/exports/exportToSvg.js.map +2 -2
  41. package/dist-cjs/lib/exports/getSvgAsImage.js +181 -29
  42. package/dist-cjs/lib/exports/getSvgAsImage.js.map +3 -3
  43. package/dist-cjs/lib/exports/getSvgJsx.js +21 -9
  44. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  45. package/dist-cjs/lib/globals/environment.js +4 -3
  46. package/dist-cjs/lib/globals/environment.js.map +2 -2
  47. package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -2
  48. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  49. package/dist-cjs/lib/hooks/useDocumentEvents.js +13 -11
  50. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  51. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js +3 -2
  52. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  53. package/dist-cjs/lib/hooks/useScreenBounds.js +10 -6
  54. package/dist-cjs/lib/hooks/useScreenBounds.js.map +2 -2
  55. package/dist-cjs/lib/hooks/useViewportHeight.js +13 -11
  56. package/dist-cjs/lib/hooks/useViewportHeight.js.map +3 -3
  57. package/dist-cjs/lib/license/Watermark.js +10 -0
  58. package/dist-cjs/lib/license/Watermark.js.map +2 -2
  59. package/dist-cjs/lib/primitives/Vec.js +35 -22
  60. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  61. package/dist-cjs/lib/primitives/geometry/Arc2d.js +6 -13
  62. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  63. package/dist-cjs/lib/primitives/geometry/Circle2d.js +31 -2
  64. package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
  65. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +9 -0
  66. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  67. package/dist-cjs/lib/primitives/geometry/CubicSpline2d.js +9 -0
  68. package/dist-cjs/lib/primitives/geometry/CubicSpline2d.js.map +2 -2
  69. package/dist-cjs/lib/primitives/geometry/Edge2d.js +32 -18
  70. package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
  71. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +12 -0
  72. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
  73. package/dist-cjs/lib/primitives/geometry/Polyline2d.js +51 -12
  74. package/dist-cjs/lib/primitives/geometry/Polyline2d.js.map +2 -2
  75. package/dist-cjs/lib/primitives/geometry/Stadium2d.js +12 -0
  76. package/dist-cjs/lib/primitives/geometry/Stadium2d.js.map +2 -2
  77. package/dist-cjs/lib/primitives/geometry/geometry.bench.js +133 -0
  78. package/dist-cjs/lib/primitives/geometry/geometry.bench.js.map +7 -0
  79. package/dist-cjs/lib/primitives/intersect.js +16 -15
  80. package/dist-cjs/lib/primitives/intersect.js.map +2 -2
  81. package/dist-cjs/lib/primitives/utils.js +0 -1
  82. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  83. package/dist-cjs/lib/utils/browserCanvasMaxSize.js +3 -2
  84. package/dist-cjs/lib/utils/browserCanvasMaxSize.js.map +2 -2
  85. package/dist-cjs/lib/utils/dom.js +15 -2
  86. package/dist-cjs/lib/utils/dom.js.map +2 -2
  87. package/dist-cjs/version.js +3 -3
  88. package/dist-cjs/version.js.map +1 -1
  89. package/dist-esm/index.d.mts +37 -6
  90. package/dist-esm/index.mjs +8 -1
  91. package/dist-esm/index.mjs.map +2 -2
  92. package/dist-esm/lib/TldrawEditor.mjs +7 -5
  93. package/dist-esm/lib/TldrawEditor.mjs.map +3 -3
  94. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs +2 -1
  95. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs.map +2 -2
  96. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -1
  97. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  98. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +8 -5
  99. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
  100. package/dist-esm/lib/config/TLSessionStateSnapshot.mjs +8 -5
  101. package/dist-esm/lib/config/TLSessionStateSnapshot.mjs.map +2 -2
  102. package/dist-esm/lib/config/TLUserPreferences.mjs +3 -2
  103. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  104. package/dist-esm/lib/config/createTLStore.mjs +1 -0
  105. package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
  106. package/dist-esm/lib/editor/Editor.mjs +53 -17
  107. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  108. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +64 -6
  109. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
  110. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +4 -3
  111. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
  112. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +5 -0
  113. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +2 -2
  114. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +2 -2
  115. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  116. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +3 -2
  117. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  118. package/dist-esm/lib/exports/FontEmbedder.mjs +9 -8
  119. package/dist-esm/lib/exports/FontEmbedder.mjs.map +2 -2
  120. package/dist-esm/lib/exports/StyleEmbedder.mjs +29 -16
  121. package/dist-esm/lib/exports/StyleEmbedder.mjs.map +3 -3
  122. package/dist-esm/lib/exports/domUtils.mjs +15 -0
  123. package/dist-esm/lib/exports/domUtils.mjs.map +2 -2
  124. package/dist-esm/lib/exports/embedMedia.mjs +16 -13
  125. package/dist-esm/lib/exports/embedMedia.mjs.map +2 -2
  126. package/dist-esm/lib/exports/exportToSvg.mjs +8 -7
  127. package/dist-esm/lib/exports/exportToSvg.mjs.map +2 -2
  128. package/dist-esm/lib/exports/getSvgAsImage.mjs +181 -29
  129. package/dist-esm/lib/exports/getSvgAsImage.mjs.map +3 -3
  130. package/dist-esm/lib/exports/getSvgJsx.mjs +21 -9
  131. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  132. package/dist-esm/lib/globals/environment.mjs +4 -3
  133. package/dist-esm/lib/globals/environment.mjs.map +2 -2
  134. package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -2
  135. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  136. package/dist-esm/lib/hooks/useDocumentEvents.mjs +13 -11
  137. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  138. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs +3 -2
  139. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  140. package/dist-esm/lib/hooks/useScreenBounds.mjs +10 -6
  141. package/dist-esm/lib/hooks/useScreenBounds.mjs.map +2 -2
  142. package/dist-esm/lib/hooks/useViewportHeight.mjs +13 -11
  143. package/dist-esm/lib/hooks/useViewportHeight.mjs.map +3 -3
  144. package/dist-esm/lib/license/Watermark.mjs +10 -0
  145. package/dist-esm/lib/license/Watermark.mjs.map +2 -2
  146. package/dist-esm/lib/primitives/Vec.mjs +35 -22
  147. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  148. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +6 -13
  149. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  150. package/dist-esm/lib/primitives/geometry/Circle2d.mjs +31 -2
  151. package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
  152. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +9 -0
  153. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  154. package/dist-esm/lib/primitives/geometry/CubicSpline2d.mjs +9 -0
  155. package/dist-esm/lib/primitives/geometry/CubicSpline2d.mjs.map +2 -2
  156. package/dist-esm/lib/primitives/geometry/Edge2d.mjs +32 -18
  157. package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
  158. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs +13 -1
  159. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
  160. package/dist-esm/lib/primitives/geometry/Polyline2d.mjs +51 -12
  161. package/dist-esm/lib/primitives/geometry/Polyline2d.mjs.map +2 -2
  162. package/dist-esm/lib/primitives/geometry/Stadium2d.mjs +13 -1
  163. package/dist-esm/lib/primitives/geometry/Stadium2d.mjs.map +2 -2
  164. package/dist-esm/lib/primitives/geometry/geometry.bench.mjs +132 -0
  165. package/dist-esm/lib/primitives/geometry/geometry.bench.mjs.map +7 -0
  166. package/dist-esm/lib/primitives/intersect.mjs +17 -16
  167. package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
  168. package/dist-esm/lib/primitives/utils.mjs +0 -1
  169. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  170. package/dist-esm/lib/utils/browserCanvasMaxSize.mjs +3 -2
  171. package/dist-esm/lib/utils/browserCanvasMaxSize.mjs.map +2 -2
  172. package/dist-esm/lib/utils/dom.mjs +15 -2
  173. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  174. package/dist-esm/version.mjs +3 -3
  175. package/dist-esm/version.mjs.map +1 -1
  176. package/package.json +7 -7
  177. package/src/index.ts +3 -0
  178. package/src/lib/TldrawEditor.tsx +7 -5
  179. package/src/lib/components/default-components/CanvasShapeIndicators.tsx +2 -1
  180. package/src/lib/components/default-components/DefaultCanvas.tsx +1 -1
  181. package/src/lib/components/default-components/DefaultErrorFallback.tsx +8 -5
  182. package/src/lib/config/TLSessionStateSnapshot.ts +8 -5
  183. package/src/lib/config/TLUserPreferences.ts +3 -2
  184. package/src/lib/config/createTLStore.ts +3 -0
  185. package/src/lib/editor/Editor.ts +53 -15
  186. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +7 -6
  187. package/src/lib/editor/managers/FocusManager/FocusManager.ts +10 -7
  188. package/src/lib/editor/managers/FontManager/FontManager.test.ts +1 -0
  189. package/src/lib/editor/managers/FontManager/FontManager.ts +4 -3
  190. package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +16 -0
  191. package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +7 -2
  192. package/src/lib/editor/managers/TextManager/TextManager.test.ts +4 -5
  193. package/src/lib/editor/managers/TextManager/TextManager.ts +2 -2
  194. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +3 -2
  195. package/src/lib/editor/types/misc-types.ts +8 -2
  196. package/src/lib/exports/FontEmbedder.ts +10 -9
  197. package/src/lib/exports/StyleEmbedder.ts +33 -15
  198. package/src/lib/exports/domUtils.ts +20 -0
  199. package/src/lib/exports/embedMedia.ts +23 -17
  200. package/src/lib/exports/exportToSvg.tsx +8 -7
  201. package/src/lib/exports/getSvgAsImage.ts +292 -32
  202. package/src/lib/exports/getSvgJsx.test.ts +103 -101
  203. package/src/lib/exports/getSvgJsx.tsx +33 -10
  204. package/src/lib/globals/environment.ts +4 -3
  205. package/src/lib/hooks/useCanvasEvents.ts +2 -3
  206. package/src/lib/hooks/useDocumentEvents.ts +16 -11
  207. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +3 -3
  208. package/src/lib/hooks/useScreenBounds.ts +10 -6
  209. package/src/lib/hooks/useViewportHeight.ts +13 -11
  210. package/src/lib/license/Watermark.tsx +10 -0
  211. package/src/lib/primitives/Vec.ts +51 -24
  212. package/src/lib/primitives/geometry/Arc2d.ts +10 -15
  213. package/src/lib/primitives/geometry/Circle2d.ts +40 -2
  214. package/src/lib/primitives/geometry/CubicBezier2d.ts +10 -0
  215. package/src/lib/primitives/geometry/CubicSpline2d.ts +10 -0
  216. package/src/lib/primitives/geometry/Edge2d.ts +41 -18
  217. package/src/lib/primitives/geometry/Ellipse2d.ts +14 -1
  218. package/src/lib/primitives/geometry/Polyline2d.ts +60 -12
  219. package/src/lib/primitives/geometry/Stadium2d.ts +14 -1
  220. package/src/lib/primitives/geometry/geometry.bench.ts +179 -0
  221. package/src/lib/primitives/intersect.ts +27 -27
  222. package/src/lib/primitives/utils.ts +4 -4
  223. package/src/lib/test/TestEditor.ts +1 -0
  224. package/src/lib/utils/browserCanvasMaxSize.ts +4 -2
  225. package/src/lib/utils/dom.ts +34 -2
  226. 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 { type, width, height, quality = 1, pixelRatio = 2 } = options
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
- const canvas = await new Promise<HTMLCanvasElement | null>((resolve) => {
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 = document.createElement('canvas') as HTMLCanvasElement
111
+ const canvas = getGlobalDocument().createElement('canvas') as HTMLCanvasElement
46
112
  const ctx = canvas.getContext('2d')!
47
-
48
- canvas.width = clampedWidth
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, clampedWidth, clampedHeight)
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
- if (!canvas) return null
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 blob = await new Promise<Blob | null>((resolve) =>
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 (!blob) return null
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
- if (type === 'png') {
85
- const view = new DataView(await blob.arrayBuffer())
86
- return PngHelpers.setPhysChunk(view, effectiveScale, {
87
- type: 'image/' + type,
88
- })
89
- } else {
90
- return blob
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
  }