@tldraw/utils 4.5.2 → 4.6.0-canary.b9b3bfdc07c7

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.
@@ -939,6 +939,7 @@ export declare class MediaHelpers {
939
939
  * Load a video element from a URL with cross-origin support.
940
940
  *
941
941
  * @param src - The URL of the video to load
942
+ * @param doc - Optional document to create the video element in
942
943
  * @returns Promise that resolves to the loaded HTMLVideoElement
943
944
  * @example
944
945
  * ```ts
@@ -947,7 +948,7 @@ export declare class MediaHelpers {
947
948
  * ```
948
949
  * @public
949
950
  */
950
- static loadVideo(src: string): Promise<HTMLVideoElement>;
951
+ static loadVideo(src: string, doc?: Document): Promise<HTMLVideoElement>;
951
952
  /**
952
953
  * Extract a frame from a video element as a data URL.
953
954
  *
@@ -969,6 +970,7 @@ export declare class MediaHelpers {
969
970
  * Load an image from a URL and get its dimensions along with the image element.
970
971
  *
971
972
  * @param src - The URL of the image to load
973
+ * @param doc - Optional document to use for DOM operations (e.g. measuring SVG dimensions)
972
974
  * @returns Promise that resolves to an object with width, height, and the image element
973
975
  * @example
974
976
  * ```ts
@@ -979,7 +981,7 @@ export declare class MediaHelpers {
979
981
  * ```
980
982
  * @public
981
983
  */
982
- static getImageAndDimensions(src: string): Promise<{
984
+ static getImageAndDimensions(src: string, doc?: Document): Promise<{
983
985
  h: number;
984
986
  image: HTMLImageElement;
985
987
  w: number;
@@ -988,6 +990,7 @@ export declare class MediaHelpers {
988
990
  * Get the size of a video blob
989
991
  *
990
992
  * @param blob - A Blob containing the video
993
+ * @param doc - Optional document to create elements in
991
994
  * @returns Promise that resolves to an object with width and height properties
992
995
  * @example
993
996
  * ```ts
@@ -997,7 +1000,7 @@ export declare class MediaHelpers {
997
1000
  * ```
998
1001
  * @public
999
1002
  */
1000
- static getVideoSize(blob: Blob): Promise<{
1003
+ static getVideoSize(blob: Blob, doc?: Document): Promise<{
1001
1004
  h: number;
1002
1005
  w: number;
1003
1006
  }>;
@@ -1005,6 +1008,7 @@ export declare class MediaHelpers {
1005
1008
  * Get the size of an image blob
1006
1009
  *
1007
1010
  * @param blob - A Blob containing the image
1011
+ * @param doc - Optional document to use for DOM operations
1008
1012
  * @returns Promise that resolves to an object with width and height properties
1009
1013
  * @example
1010
1014
  * ```ts
@@ -1014,7 +1018,7 @@ export declare class MediaHelpers {
1014
1018
  * ```
1015
1019
  * @public
1016
1020
  */
1017
- static getImageSize(blob: Blob): Promise<{
1021
+ static getImageSize(blob: Blob, doc?: Document): Promise<{
1018
1022
  h: number;
1019
1023
  pixelRatio: number;
1020
1024
  w: number;
package/dist-cjs/index.js CHANGED
@@ -169,7 +169,7 @@ var import_version2 = require("./lib/version");
169
169
  var import_warn = require("./lib/warn");
170
170
  (0, import_version.registerTldrawLibraryVersion)(
171
171
  "@tldraw/utils",
172
- "4.5.2",
172
+ "4.6.0-canary.b9b3bfdc07c7",
173
173
  "cjs"
174
174
  );
175
175
  //# sourceMappingURL=index.js.map
@@ -66,6 +66,7 @@ class MediaHelpers {
66
66
  * Load a video element from a URL with cross-origin support.
67
67
  *
68
68
  * @param src - The URL of the video to load
69
+ * @param doc - Optional document to create the video element in
69
70
  * @returns Promise that resolves to the loaded HTMLVideoElement
70
71
  * @example
71
72
  * ```ts
@@ -74,9 +75,9 @@ class MediaHelpers {
74
75
  * ```
75
76
  * @public
76
77
  */
77
- static loadVideo(src) {
78
+ static loadVideo(src, doc) {
78
79
  return new Promise((resolve, reject) => {
79
- const video = document.createElement("video");
80
+ const video = (doc ?? document).createElement("video");
80
81
  video.onloadeddata = () => resolve(video);
81
82
  video.onerror = (e) => {
82
83
  console.error(e);
@@ -115,7 +116,7 @@ class MediaHelpers {
115
116
  }
116
117
  }
117
118
  if (video.readyState >= video.HAVE_CURRENT_DATA) {
118
- const canvas = document.createElement("canvas");
119
+ const canvas = (video.ownerDocument ?? document).createElement("canvas");
119
120
  canvas.width = video.videoWidth;
120
121
  canvas.height = video.videoHeight;
121
122
  const ctx = canvas.getContext("2d");
@@ -152,6 +153,7 @@ class MediaHelpers {
152
153
  * Load an image from a URL and get its dimensions along with the image element.
153
154
  *
154
155
  * @param src - The URL of the image to load
156
+ * @param doc - Optional document to use for DOM operations (e.g. measuring SVG dimensions)
155
157
  * @returns Promise that resolves to an object with width, height, and the image element
156
158
  * @example
157
159
  * ```ts
@@ -162,7 +164,7 @@ class MediaHelpers {
162
164
  * ```
163
165
  * @public
164
166
  */
165
- static getImageAndDimensions(src) {
167
+ static getImageAndDimensions(src, doc) {
166
168
  return new Promise((resolve, reject) => {
167
169
  const img = (0, import_network.Image)();
168
170
  img.onload = () => {
@@ -173,12 +175,13 @@ class MediaHelpers {
173
175
  h: img.naturalHeight
174
176
  };
175
177
  } else {
176
- document.body.appendChild(img);
178
+ const body = (doc ?? document).body;
179
+ body.appendChild(img);
177
180
  dimensions = {
178
181
  w: img.clientWidth,
179
182
  h: img.clientHeight
180
183
  };
181
- document.body.removeChild(img);
184
+ body.removeChild(img);
182
185
  }
183
186
  resolve({ ...dimensions, image: img });
184
187
  };
@@ -199,6 +202,7 @@ class MediaHelpers {
199
202
  * Get the size of a video blob
200
203
  *
201
204
  * @param blob - A Blob containing the video
205
+ * @param doc - Optional document to create elements in
202
206
  * @returns Promise that resolves to an object with width and height properties
203
207
  * @example
204
208
  * ```ts
@@ -208,9 +212,9 @@ class MediaHelpers {
208
212
  * ```
209
213
  * @public
210
214
  */
211
- static async getVideoSize(blob) {
215
+ static async getVideoSize(blob, doc) {
212
216
  return MediaHelpers.usingObjectURL(blob, async (url) => {
213
- const video = await MediaHelpers.loadVideo(url);
217
+ const video = await MediaHelpers.loadVideo(url, doc);
214
218
  return { w: video.videoWidth, h: video.videoHeight };
215
219
  });
216
220
  }
@@ -218,6 +222,7 @@ class MediaHelpers {
218
222
  * Get the size of an image blob
219
223
  *
220
224
  * @param blob - A Blob containing the image
225
+ * @param doc - Optional document to use for DOM operations
221
226
  * @returns Promise that resolves to an object with width and height properties
222
227
  * @example
223
228
  * ```ts
@@ -227,8 +232,11 @@ class MediaHelpers {
227
232
  * ```
228
233
  * @public
229
234
  */
230
- static async getImageSize(blob) {
231
- const { w, h } = await MediaHelpers.usingObjectURL(blob, MediaHelpers.getImageAndDimensions);
235
+ static async getImageSize(blob, doc) {
236
+ const { w, h } = await MediaHelpers.usingObjectURL(
237
+ blob,
238
+ (url) => MediaHelpers.getImageAndDimensions(url, doc)
239
+ );
232
240
  try {
233
241
  if (blob.type === "image/png") {
234
242
  const view = new DataView(await blob.arrayBuffer());
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/media/media.ts"],
4
- "sourcesContent": ["import { promiseWithResolve } from '../control'\nimport { Image } from '../network'\nimport { isApngAnimated } from './apng'\nimport { isAvifAnimated } from './avif'\nimport { isGifAnimated } from './gif'\nimport { PngHelpers } from './png'\nimport { isWebpAnimated } from './webp'\n\n/**\n * Array of supported vector image MIME types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES } from '@tldraw/utils'\n *\n * const isSvg = DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES.includes('image/svg+xml')\n * console.log(isSvg) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES = Object.freeze(['image/svg+xml' as const])\n/**\n * Array of supported static (non-animated) image MIME types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES } from '@tldraw/utils'\n *\n * const isStatic = DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES.includes('image/jpeg')\n * console.log(isStatic) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES = Object.freeze([\n\t'image/jpeg' as const,\n\t'image/png' as const,\n\t'image/webp' as const,\n])\n/**\n * Array of supported animated image MIME types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES } from '@tldraw/utils'\n *\n * const isAnimated = DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES.includes('image/gif')\n * console.log(isAnimated) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES = Object.freeze([\n\t'image/gif' as const,\n\t'image/apng' as const,\n\t'image/avif' as const,\n])\n/**\n * Array of all supported image MIME types, combining static, vector, and animated types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_IMAGE_TYPES } from '@tldraw/utils'\n *\n * const isSupported = DEFAULT_SUPPORTED_IMAGE_TYPES.includes('image/png')\n * console.log(isSupported) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_IMAGE_TYPES = Object.freeze([\n\t...DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES,\n\t...DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES,\n\t...DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES,\n])\n/**\n * Array of supported video MIME types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORT_VIDEO_TYPES } from '@tldraw/utils'\n *\n * const isVideo = DEFAULT_SUPPORT_VIDEO_TYPES.includes('video/mp4')\n * console.log(isVideo) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORT_VIDEO_TYPES = Object.freeze([\n\t'video/mp4' as const,\n\t'video/webm' as const,\n\t'video/quicktime' as const,\n])\n/**\n * Array of all supported media MIME types, combining images and videos.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_MEDIA_TYPES } from '@tldraw/utils'\n *\n * const isMediaFile = DEFAULT_SUPPORTED_MEDIA_TYPES.includes('video/mp4')\n * console.log(isMediaFile) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_MEDIA_TYPES = Object.freeze([\n\t...DEFAULT_SUPPORTED_IMAGE_TYPES,\n\t...DEFAULT_SUPPORT_VIDEO_TYPES,\n])\n/**\n * Comma-separated string of all supported media MIME types, useful for HTML file input accept attributes.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_MEDIA_TYPE_LIST } from '@tldraw/utils'\n *\n * // Use in HTML file input for media uploads\n * const input = document.createElement('input')\n * input.type = 'file'\n * input.accept = DEFAULT_SUPPORTED_MEDIA_TYPE_LIST\n * input.addEventListener('change', (e) => {\n * const files = (e.target as HTMLInputElement).files\n * if (files) console.log(`Selected ${files.length} file(s)`)\n * })\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_MEDIA_TYPE_LIST = DEFAULT_SUPPORTED_MEDIA_TYPES.join(',')\n\n/**\n * Helpers for media\n *\n * @public\n */\nexport class MediaHelpers {\n\t/**\n\t * Load a video element from a URL with cross-origin support.\n\t *\n\t * @param src - The URL of the video to load\n\t * @returns Promise that resolves to the loaded HTMLVideoElement\n\t * @example\n\t * ```ts\n\t * const video = await MediaHelpers.loadVideo('https://example.com/video.mp4')\n\t * console.log(`Video dimensions: ${video.videoWidth}x${video.videoHeight}`)\n\t * ```\n\t * @public\n\t */\n\tstatic loadVideo(src: string): Promise<HTMLVideoElement> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst video = document.createElement('video')\n\t\t\tvideo.onloadeddata = () => resolve(video)\n\t\t\tvideo.onerror = (e) => {\n\t\t\t\tconsole.error(e)\n\t\t\t\treject(new Error('Could not load video'))\n\t\t\t}\n\t\t\tvideo.crossOrigin = 'anonymous'\n\t\t\tvideo.src = src\n\t\t})\n\t}\n\n\t/**\n\t * Extract a frame from a video element as a data URL.\n\t *\n\t * @param video - The HTMLVideoElement to extract frame from\n\t * @param time - The time in seconds to extract the frame from (default: 0)\n\t * @returns Promise that resolves to a data URL of the video frame\n\t * @example\n\t * ```ts\n\t * const video = await MediaHelpers.loadVideo('https://example.com/video.mp4')\n\t * const frameDataUrl = await MediaHelpers.getVideoFrameAsDataUrl(video, 5.0)\n\t * // Use frameDataUrl as image thumbnail\n\t * const img = document.createElement('img')\n\t * img.src = frameDataUrl\n\t * ```\n\t * @public\n\t */\n\tstatic async getVideoFrameAsDataUrl(video: HTMLVideoElement, time = 0): Promise<string> {\n\t\tconst promise = promiseWithResolve<string>()\n\t\tlet didSetTime = false\n\n\t\tconst onReadyStateChanged = () => {\n\t\t\tif (!didSetTime) {\n\t\t\t\tif (video.readyState >= video.HAVE_METADATA) {\n\t\t\t\t\tdidSetTime = true\n\t\t\t\t\tvideo.currentTime = time\n\t\t\t\t} else {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (video.readyState >= video.HAVE_CURRENT_DATA) {\n\t\t\t\tconst canvas = document.createElement('canvas')\n\t\t\t\tcanvas.width = video.videoWidth\n\t\t\t\tcanvas.height = video.videoHeight\n\t\t\t\tconst ctx = canvas.getContext('2d')\n\t\t\t\tif (!ctx) {\n\t\t\t\t\tthrow new Error('Could not get 2d context')\n\t\t\t\t}\n\t\t\t\tctx.drawImage(video, 0, 0)\n\t\t\t\tpromise.resolve(canvas.toDataURL())\n\t\t\t}\n\t\t}\n\t\tconst onError = (e: Event) => {\n\t\t\tconsole.error(e)\n\t\t\tpromise.reject(new Error('Could not get video frame'))\n\t\t}\n\n\t\tvideo.addEventListener('loadedmetadata', onReadyStateChanged)\n\t\tvideo.addEventListener('loadeddata', onReadyStateChanged)\n\t\tvideo.addEventListener('canplay', onReadyStateChanged)\n\t\tvideo.addEventListener('seeked', onReadyStateChanged)\n\n\t\tvideo.addEventListener('error', onError)\n\t\tvideo.addEventListener('stalled', onError)\n\n\t\tonReadyStateChanged()\n\n\t\ttry {\n\t\t\treturn await promise\n\t\t} finally {\n\t\t\tvideo.removeEventListener('loadedmetadata', onReadyStateChanged)\n\t\t\tvideo.removeEventListener('loadeddata', onReadyStateChanged)\n\t\t\tvideo.removeEventListener('canplay', onReadyStateChanged)\n\t\t\tvideo.removeEventListener('seeked', onReadyStateChanged)\n\n\t\t\tvideo.removeEventListener('error', onError)\n\t\t\tvideo.removeEventListener('stalled', onError)\n\t\t}\n\t}\n\n\t/**\n\t * Load an image from a URL and get its dimensions along with the image element.\n\t *\n\t * @param src - The URL of the image to load\n\t * @returns Promise that resolves to an object with width, height, and the image element\n\t * @example\n\t * ```ts\n\t * const { w, h, image } = await MediaHelpers.getImageAndDimensions('https://example.com/image.png')\n\t * console.log(`Image size: ${w}x${h}`)\n\t * // Image is ready to use\n\t * document.body.appendChild(image)\n\t * ```\n\t * @public\n\t */\n\tstatic getImageAndDimensions(\n\t\tsrc: string\n\t): Promise<{ w: number; h: number; image: HTMLImageElement }> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst img = Image()\n\t\t\timg.onload = () => {\n\t\t\t\tlet dimensions\n\t\t\t\tif (img.naturalWidth) {\n\t\t\t\t\tdimensions = {\n\t\t\t\t\t\tw: img.naturalWidth,\n\t\t\t\t\t\th: img.naturalHeight,\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Sigh, Firefox doesn't have naturalWidth or naturalHeight for SVGs. :-/\n\t\t\t\t\t// We have to attach to dom and use clientWidth/clientHeight.\n\t\t\t\t\tdocument.body.appendChild(img)\n\t\t\t\t\tdimensions = {\n\t\t\t\t\t\tw: img.clientWidth,\n\t\t\t\t\t\th: img.clientHeight,\n\t\t\t\t\t}\n\t\t\t\t\tdocument.body.removeChild(img)\n\t\t\t\t}\n\t\t\t\tresolve({ ...dimensions, image: img })\n\t\t\t}\n\t\t\timg.onerror = (e) => {\n\t\t\t\tconsole.error(e)\n\t\t\t\treject(new Error('Could not load image'))\n\t\t\t}\n\t\t\timg.crossOrigin = 'anonymous'\n\t\t\timg.referrerPolicy = 'strict-origin-when-cross-origin'\n\t\t\timg.style.visibility = 'hidden'\n\t\t\timg.style.position = 'absolute'\n\t\t\timg.style.opacity = '0'\n\t\t\timg.style.zIndex = '-9999'\n\t\t\timg.src = src\n\t\t})\n\t}\n\n\t/**\n\t * Get the size of a video blob\n\t *\n\t * @param blob - A Blob containing the video\n\t * @returns Promise that resolves to an object with width and height properties\n\t * @example\n\t * ```ts\n\t * const file = new File([...], 'video.mp4', { type: 'video/mp4' })\n\t * const { w, h } = await MediaHelpers.getVideoSize(file)\n\t * console.log(`Video dimensions: ${w}x${h}`)\n\t * ```\n\t * @public\n\t */\n\tstatic async getVideoSize(blob: Blob): Promise<{ w: number; h: number }> {\n\t\treturn MediaHelpers.usingObjectURL(blob, async (url) => {\n\t\t\tconst video = await MediaHelpers.loadVideo(url)\n\t\t\treturn { w: video.videoWidth, h: video.videoHeight }\n\t\t})\n\t}\n\n\t/**\n\t * Get the size of an image blob\n\t *\n\t * @param blob - A Blob containing the image\n\t * @returns Promise that resolves to an object with width and height properties\n\t * @example\n\t * ```ts\n\t * const file = new File([...], 'image.png', { type: 'image/png' })\n\t * const { w, h } = await MediaHelpers.getImageSize(file)\n\t * console.log(`Image dimensions: ${w}x${h}`)\n\t * ```\n\t * @public\n\t */\n\tstatic async getImageSize(blob: Blob): Promise<{ w: number; h: number; pixelRatio: number }> {\n\t\tconst { w, h } = await MediaHelpers.usingObjectURL(blob, MediaHelpers.getImageAndDimensions)\n\n\t\ttry {\n\t\t\tif (blob.type === 'image/png') {\n\t\t\t\tconst view = new DataView(await blob.arrayBuffer())\n\t\t\t\tif (PngHelpers.isPng(view, 0)) {\n\t\t\t\t\tconst physChunk = PngHelpers.findChunk(view, 'pHYs')\n\t\t\t\t\tif (physChunk) {\n\t\t\t\t\t\tconst physData = PngHelpers.parsePhys(view, physChunk.dataOffset)\n\t\t\t\t\t\tif (physData.unit === 1 && physData.ppux === physData.ppuy) {\n\t\t\t\t\t\t\tconst dpi = Math.round(physData.ppux * 0.0254)\n\t\t\t\t\t\t\t// Try both standard baselines: Windows/web = 96, macOS = 72.\n\t\t\t\t\t\t\t// Pick whichever yields a clean integer ratio > 1.\n\t\t\t\t\t\t\tconst r96 = dpi / 96\n\t\t\t\t\t\t\tconst r72 = dpi / 72\n\t\t\t\t\t\t\tlet pixelRatio = 1\n\t\t\t\t\t\t\tif (Number.isInteger(r96) && r96 > 1) {\n\t\t\t\t\t\t\t\tpixelRatio = r96\n\t\t\t\t\t\t\t} else if (Number.isInteger(r72) && r72 > 1) {\n\t\t\t\t\t\t\t\tpixelRatio = r72\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (pixelRatio > 1) {\n\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\tw: Math.ceil(w / pixelRatio),\n\t\t\t\t\t\t\t\t\th: Math.ceil(h / pixelRatio),\n\t\t\t\t\t\t\t\t\tpixelRatio,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tconsole.error(err)\n\t\t\treturn { w, h, pixelRatio: 1 }\n\t\t}\n\t\treturn { w, h, pixelRatio: 1 }\n\t}\n\n\t/**\n\t * Check if a media file blob contains animation data.\n\t *\n\t * @param file - The Blob to check for animation\n\t * @returns Promise that resolves to true if the file is animated, false otherwise\n\t * @example\n\t * ```ts\n\t * const file = new File([...], 'animation.gif', { type: 'image/gif' })\n\t * const animated = await MediaHelpers.isAnimated(file)\n\t * console.log(animated ? 'Animated' : 'Static')\n\t * ```\n\t * @public\n\t */\n\tstatic async isAnimated(file: Blob): Promise<boolean> {\n\t\tif (file.type === 'image/gif') {\n\t\t\treturn isGifAnimated(await file.arrayBuffer())\n\t\t}\n\n\t\tif (file.type === 'image/avif') {\n\t\t\treturn isAvifAnimated(await file.arrayBuffer())\n\t\t}\n\n\t\tif (file.type === 'image/webp') {\n\t\t\treturn isWebpAnimated(await file.arrayBuffer())\n\t\t}\n\n\t\tif (file.type === 'image/apng') {\n\t\t\treturn isApngAnimated(await file.arrayBuffer())\n\t\t}\n\n\t\treturn false\n\t}\n\n\t/**\n\t * Check if a MIME type represents an animated image format.\n\t *\n\t * @param mimeType - The MIME type to check\n\t * @returns True if the MIME type is an animated image format, false otherwise\n\t * @example\n\t * ```ts\n\t * const isAnimated = MediaHelpers.isAnimatedImageType('image/gif')\n\t * console.log(isAnimated) // true\n\t * ```\n\t * @public\n\t */\n\tstatic isAnimatedImageType(mimeType: string | null): boolean {\n\t\treturn DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES.includes((mimeType as any) || '')\n\t}\n\n\t/**\n\t * Check if a MIME type represents a static (non-animated) image format.\n\t *\n\t * @param mimeType - The MIME type to check\n\t * @returns True if the MIME type is a static image format, false otherwise\n\t * @example\n\t * ```ts\n\t * const isStatic = MediaHelpers.isStaticImageType('image/jpeg')\n\t * console.log(isStatic) // true\n\t * ```\n\t * @public\n\t */\n\tstatic isStaticImageType(mimeType: string | null): boolean {\n\t\treturn DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES.includes((mimeType as any) || '')\n\t}\n\n\t/**\n\t * Check if a MIME type represents a vector image format.\n\t *\n\t * @param mimeType - The MIME type to check\n\t * @returns True if the MIME type is a vector image format, false otherwise\n\t * @example\n\t * ```ts\n\t * const isVector = MediaHelpers.isVectorImageType('image/svg+xml')\n\t * console.log(isVector) // true\n\t * ```\n\t * @public\n\t */\n\tstatic isVectorImageType(mimeType: string | null): boolean {\n\t\treturn DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES.includes((mimeType as any) || '')\n\t}\n\n\t/**\n\t * Check if a MIME type represents any supported image format (static, animated, or vector).\n\t *\n\t * @param mimeType - The MIME type to check\n\t * @returns True if the MIME type is a supported image format, false otherwise\n\t * @example\n\t * ```ts\n\t * const isImage = MediaHelpers.isImageType('image/png')\n\t * console.log(isImage) // true\n\t * ```\n\t * @public\n\t */\n\tstatic isImageType(mimeType: string): boolean {\n\t\treturn DEFAULT_SUPPORTED_IMAGE_TYPES.includes((mimeType as any) || '')\n\t}\n\n\t/**\n\t * Utility function to create an object URL from a blob, execute a function with it, and automatically clean it up.\n\t *\n\t * @param blob - The Blob to create an object URL for\n\t * @param fn - Function to execute with the object URL\n\t * @returns Promise that resolves to the result of the function\n\t * @example\n\t * ```ts\n\t * const result = await MediaHelpers.usingObjectURL(imageBlob, async (url) => {\n\t * const { w, h } = await MediaHelpers.getImageAndDimensions(url)\n\t * return { width: w, height: h }\n\t * })\n\t * // Object URL is automatically revoked after function completes\n\t * console.log(`Image dimensions: ${result.width}x${result.height}`)\n\t * ```\n\t * @public\n\t */\n\tstatic async usingObjectURL<T>(blob: Blob, fn: (url: string) => Promise<T>): Promise<T> {\n\t\tconst url = URL.createObjectURL(blob)\n\t\ttry {\n\t\t\treturn await fn(url)\n\t\t} finally {\n\t\t\tURL.revokeObjectURL(url)\n\t\t}\n\t}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { promiseWithResolve } from '../control'\nimport { Image } from '../network'\nimport { isApngAnimated } from './apng'\nimport { isAvifAnimated } from './avif'\nimport { isGifAnimated } from './gif'\nimport { PngHelpers } from './png'\nimport { isWebpAnimated } from './webp'\n\n/**\n * Array of supported vector image MIME types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES } from '@tldraw/utils'\n *\n * const isSvg = DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES.includes('image/svg+xml')\n * console.log(isSvg) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES = Object.freeze(['image/svg+xml' as const])\n/**\n * Array of supported static (non-animated) image MIME types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES } from '@tldraw/utils'\n *\n * const isStatic = DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES.includes('image/jpeg')\n * console.log(isStatic) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES = Object.freeze([\n\t'image/jpeg' as const,\n\t'image/png' as const,\n\t'image/webp' as const,\n])\n/**\n * Array of supported animated image MIME types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES } from '@tldraw/utils'\n *\n * const isAnimated = DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES.includes('image/gif')\n * console.log(isAnimated) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES = Object.freeze([\n\t'image/gif' as const,\n\t'image/apng' as const,\n\t'image/avif' as const,\n])\n/**\n * Array of all supported image MIME types, combining static, vector, and animated types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_IMAGE_TYPES } from '@tldraw/utils'\n *\n * const isSupported = DEFAULT_SUPPORTED_IMAGE_TYPES.includes('image/png')\n * console.log(isSupported) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_IMAGE_TYPES = Object.freeze([\n\t...DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES,\n\t...DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES,\n\t...DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES,\n])\n/**\n * Array of supported video MIME types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORT_VIDEO_TYPES } from '@tldraw/utils'\n *\n * const isVideo = DEFAULT_SUPPORT_VIDEO_TYPES.includes('video/mp4')\n * console.log(isVideo) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORT_VIDEO_TYPES = Object.freeze([\n\t'video/mp4' as const,\n\t'video/webm' as const,\n\t'video/quicktime' as const,\n])\n/**\n * Array of all supported media MIME types, combining images and videos.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_MEDIA_TYPES } from '@tldraw/utils'\n *\n * const isMediaFile = DEFAULT_SUPPORTED_MEDIA_TYPES.includes('video/mp4')\n * console.log(isMediaFile) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_MEDIA_TYPES = Object.freeze([\n\t...DEFAULT_SUPPORTED_IMAGE_TYPES,\n\t...DEFAULT_SUPPORT_VIDEO_TYPES,\n])\n/**\n * Comma-separated string of all supported media MIME types, useful for HTML file input accept attributes.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_MEDIA_TYPE_LIST } from '@tldraw/utils'\n *\n * // Use in HTML file input for media uploads\n * const input = document.createElement('input')\n * input.type = 'file'\n * input.accept = DEFAULT_SUPPORTED_MEDIA_TYPE_LIST\n * input.addEventListener('change', (e) => {\n * const files = (e.target as HTMLInputElement).files\n * if (files) console.log(`Selected ${files.length} file(s)`)\n * })\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_MEDIA_TYPE_LIST = DEFAULT_SUPPORTED_MEDIA_TYPES.join(',')\n\n/**\n * Helpers for media\n *\n * @public\n */\nexport class MediaHelpers {\n\t/**\n\t * Load a video element from a URL with cross-origin support.\n\t *\n\t * @param src - The URL of the video to load\n\t * @param doc - Optional document to create the video element in\n\t * @returns Promise that resolves to the loaded HTMLVideoElement\n\t * @example\n\t * ```ts\n\t * const video = await MediaHelpers.loadVideo('https://example.com/video.mp4')\n\t * console.log(`Video dimensions: ${video.videoWidth}x${video.videoHeight}`)\n\t * ```\n\t * @public\n\t */\n\tstatic loadVideo(src: string, doc?: Document): Promise<HTMLVideoElement> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\t// eslint-disable-next-line no-restricted-globals\n\t\t\tconst video = (doc ?? document).createElement('video')\n\t\t\tvideo.onloadeddata = () => resolve(video)\n\t\t\tvideo.onerror = (e) => {\n\t\t\t\tconsole.error(e)\n\t\t\t\treject(new Error('Could not load video'))\n\t\t\t}\n\t\t\tvideo.crossOrigin = 'anonymous'\n\t\t\tvideo.src = src\n\t\t})\n\t}\n\n\t/**\n\t * Extract a frame from a video element as a data URL.\n\t *\n\t * @param video - The HTMLVideoElement to extract frame from\n\t * @param time - The time in seconds to extract the frame from (default: 0)\n\t * @returns Promise that resolves to a data URL of the video frame\n\t * @example\n\t * ```ts\n\t * const video = await MediaHelpers.loadVideo('https://example.com/video.mp4')\n\t * const frameDataUrl = await MediaHelpers.getVideoFrameAsDataUrl(video, 5.0)\n\t * // Use frameDataUrl as image thumbnail\n\t * const img = document.createElement('img')\n\t * img.src = frameDataUrl\n\t * ```\n\t * @public\n\t */\n\tstatic async getVideoFrameAsDataUrl(video: HTMLVideoElement, time = 0): Promise<string> {\n\t\tconst promise = promiseWithResolve<string>()\n\t\tlet didSetTime = false\n\n\t\tconst onReadyStateChanged = () => {\n\t\t\tif (!didSetTime) {\n\t\t\t\tif (video.readyState >= video.HAVE_METADATA) {\n\t\t\t\t\tdidSetTime = true\n\t\t\t\t\tvideo.currentTime = time\n\t\t\t\t} else {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (video.readyState >= video.HAVE_CURRENT_DATA) {\n\t\t\t\t// eslint-disable-next-line no-restricted-globals\n\t\t\t\tconst canvas = (video.ownerDocument ?? document).createElement('canvas')\n\t\t\t\tcanvas.width = video.videoWidth\n\t\t\t\tcanvas.height = video.videoHeight\n\t\t\t\tconst ctx = canvas.getContext('2d')\n\t\t\t\tif (!ctx) {\n\t\t\t\t\tthrow new Error('Could not get 2d context')\n\t\t\t\t}\n\t\t\t\tctx.drawImage(video, 0, 0)\n\t\t\t\tpromise.resolve(canvas.toDataURL())\n\t\t\t}\n\t\t}\n\t\tconst onError = (e: Event) => {\n\t\t\tconsole.error(e)\n\t\t\tpromise.reject(new Error('Could not get video frame'))\n\t\t}\n\n\t\tvideo.addEventListener('loadedmetadata', onReadyStateChanged)\n\t\tvideo.addEventListener('loadeddata', onReadyStateChanged)\n\t\tvideo.addEventListener('canplay', onReadyStateChanged)\n\t\tvideo.addEventListener('seeked', onReadyStateChanged)\n\n\t\tvideo.addEventListener('error', onError)\n\t\tvideo.addEventListener('stalled', onError)\n\n\t\tonReadyStateChanged()\n\n\t\ttry {\n\t\t\treturn await promise\n\t\t} finally {\n\t\t\tvideo.removeEventListener('loadedmetadata', onReadyStateChanged)\n\t\t\tvideo.removeEventListener('loadeddata', onReadyStateChanged)\n\t\t\tvideo.removeEventListener('canplay', onReadyStateChanged)\n\t\t\tvideo.removeEventListener('seeked', onReadyStateChanged)\n\n\t\t\tvideo.removeEventListener('error', onError)\n\t\t\tvideo.removeEventListener('stalled', onError)\n\t\t}\n\t}\n\n\t/**\n\t * Load an image from a URL and get its dimensions along with the image element.\n\t *\n\t * @param src - The URL of the image to load\n\t * @param doc - Optional document to use for DOM operations (e.g. measuring SVG dimensions)\n\t * @returns Promise that resolves to an object with width, height, and the image element\n\t * @example\n\t * ```ts\n\t * const { w, h, image } = await MediaHelpers.getImageAndDimensions('https://example.com/image.png')\n\t * console.log(`Image size: ${w}x${h}`)\n\t * // Image is ready to use\n\t * document.body.appendChild(image)\n\t * ```\n\t * @public\n\t */\n\tstatic getImageAndDimensions(\n\t\tsrc: string,\n\t\tdoc?: Document\n\t): Promise<{ w: number; h: number; image: HTMLImageElement }> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst img = Image()\n\t\t\timg.onload = () => {\n\t\t\t\tlet dimensions\n\t\t\t\tif (img.naturalWidth) {\n\t\t\t\t\tdimensions = {\n\t\t\t\t\t\tw: img.naturalWidth,\n\t\t\t\t\t\th: img.naturalHeight,\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Sigh, Firefox doesn't have naturalWidth or naturalHeight for SVGs. :-/\n\t\t\t\t\t// We have to attach to dom and use clientWidth/clientHeight.\n\t\t\t\t\t// eslint-disable-next-line no-restricted-globals\n\t\t\t\t\tconst body = (doc ?? document).body\n\t\t\t\t\tbody.appendChild(img)\n\t\t\t\t\tdimensions = {\n\t\t\t\t\t\tw: img.clientWidth,\n\t\t\t\t\t\th: img.clientHeight,\n\t\t\t\t\t}\n\t\t\t\t\tbody.removeChild(img)\n\t\t\t\t}\n\t\t\t\tresolve({ ...dimensions, image: img })\n\t\t\t}\n\t\t\timg.onerror = (e) => {\n\t\t\t\tconsole.error(e)\n\t\t\t\treject(new Error('Could not load image'))\n\t\t\t}\n\t\t\timg.crossOrigin = 'anonymous'\n\t\t\timg.referrerPolicy = 'strict-origin-when-cross-origin'\n\t\t\timg.style.visibility = 'hidden'\n\t\t\timg.style.position = 'absolute'\n\t\t\timg.style.opacity = '0'\n\t\t\timg.style.zIndex = '-9999'\n\t\t\timg.src = src\n\t\t})\n\t}\n\n\t/**\n\t * Get the size of a video blob\n\t *\n\t * @param blob - A Blob containing the video\n\t * @param doc - Optional document to create elements in\n\t * @returns Promise that resolves to an object with width and height properties\n\t * @example\n\t * ```ts\n\t * const file = new File([...], 'video.mp4', { type: 'video/mp4' })\n\t * const { w, h } = await MediaHelpers.getVideoSize(file)\n\t * console.log(`Video dimensions: ${w}x${h}`)\n\t * ```\n\t * @public\n\t */\n\tstatic async getVideoSize(blob: Blob, doc?: Document): Promise<{ w: number; h: number }> {\n\t\treturn MediaHelpers.usingObjectURL(blob, async (url) => {\n\t\t\tconst video = await MediaHelpers.loadVideo(url, doc)\n\t\t\treturn { w: video.videoWidth, h: video.videoHeight }\n\t\t})\n\t}\n\n\t/**\n\t * Get the size of an image blob\n\t *\n\t * @param blob - A Blob containing the image\n\t * @param doc - Optional document to use for DOM operations\n\t * @returns Promise that resolves to an object with width and height properties\n\t * @example\n\t * ```ts\n\t * const file = new File([...], 'image.png', { type: 'image/png' })\n\t * const { w, h } = await MediaHelpers.getImageSize(file)\n\t * console.log(`Image dimensions: ${w}x${h}`)\n\t * ```\n\t * @public\n\t */\n\tstatic async getImageSize(\n\t\tblob: Blob,\n\t\tdoc?: Document\n\t): Promise<{ w: number; h: number; pixelRatio: number }> {\n\t\tconst { w, h } = await MediaHelpers.usingObjectURL(blob, (url) =>\n\t\t\tMediaHelpers.getImageAndDimensions(url, doc)\n\t\t)\n\n\t\ttry {\n\t\t\tif (blob.type === 'image/png') {\n\t\t\t\tconst view = new DataView(await blob.arrayBuffer())\n\t\t\t\tif (PngHelpers.isPng(view, 0)) {\n\t\t\t\t\tconst physChunk = PngHelpers.findChunk(view, 'pHYs')\n\t\t\t\t\tif (physChunk) {\n\t\t\t\t\t\tconst physData = PngHelpers.parsePhys(view, physChunk.dataOffset)\n\t\t\t\t\t\tif (physData.unit === 1 && physData.ppux === physData.ppuy) {\n\t\t\t\t\t\t\tconst dpi = Math.round(physData.ppux * 0.0254)\n\t\t\t\t\t\t\t// Try both standard baselines: Windows/web = 96, macOS = 72.\n\t\t\t\t\t\t\t// Pick whichever yields a clean integer ratio > 1.\n\t\t\t\t\t\t\tconst r96 = dpi / 96\n\t\t\t\t\t\t\tconst r72 = dpi / 72\n\t\t\t\t\t\t\tlet pixelRatio = 1\n\t\t\t\t\t\t\tif (Number.isInteger(r96) && r96 > 1) {\n\t\t\t\t\t\t\t\tpixelRatio = r96\n\t\t\t\t\t\t\t} else if (Number.isInteger(r72) && r72 > 1) {\n\t\t\t\t\t\t\t\tpixelRatio = r72\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (pixelRatio > 1) {\n\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\tw: Math.ceil(w / pixelRatio),\n\t\t\t\t\t\t\t\t\th: Math.ceil(h / pixelRatio),\n\t\t\t\t\t\t\t\t\tpixelRatio,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tconsole.error(err)\n\t\t\treturn { w, h, pixelRatio: 1 }\n\t\t}\n\t\treturn { w, h, pixelRatio: 1 }\n\t}\n\n\t/**\n\t * Check if a media file blob contains animation data.\n\t *\n\t * @param file - The Blob to check for animation\n\t * @returns Promise that resolves to true if the file is animated, false otherwise\n\t * @example\n\t * ```ts\n\t * const file = new File([...], 'animation.gif', { type: 'image/gif' })\n\t * const animated = await MediaHelpers.isAnimated(file)\n\t * console.log(animated ? 'Animated' : 'Static')\n\t * ```\n\t * @public\n\t */\n\tstatic async isAnimated(file: Blob): Promise<boolean> {\n\t\tif (file.type === 'image/gif') {\n\t\t\treturn isGifAnimated(await file.arrayBuffer())\n\t\t}\n\n\t\tif (file.type === 'image/avif') {\n\t\t\treturn isAvifAnimated(await file.arrayBuffer())\n\t\t}\n\n\t\tif (file.type === 'image/webp') {\n\t\t\treturn isWebpAnimated(await file.arrayBuffer())\n\t\t}\n\n\t\tif (file.type === 'image/apng') {\n\t\t\treturn isApngAnimated(await file.arrayBuffer())\n\t\t}\n\n\t\treturn false\n\t}\n\n\t/**\n\t * Check if a MIME type represents an animated image format.\n\t *\n\t * @param mimeType - The MIME type to check\n\t * @returns True if the MIME type is an animated image format, false otherwise\n\t * @example\n\t * ```ts\n\t * const isAnimated = MediaHelpers.isAnimatedImageType('image/gif')\n\t * console.log(isAnimated) // true\n\t * ```\n\t * @public\n\t */\n\tstatic isAnimatedImageType(mimeType: string | null): boolean {\n\t\treturn DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES.includes((mimeType as any) || '')\n\t}\n\n\t/**\n\t * Check if a MIME type represents a static (non-animated) image format.\n\t *\n\t * @param mimeType - The MIME type to check\n\t * @returns True if the MIME type is a static image format, false otherwise\n\t * @example\n\t * ```ts\n\t * const isStatic = MediaHelpers.isStaticImageType('image/jpeg')\n\t * console.log(isStatic) // true\n\t * ```\n\t * @public\n\t */\n\tstatic isStaticImageType(mimeType: string | null): boolean {\n\t\treturn DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES.includes((mimeType as any) || '')\n\t}\n\n\t/**\n\t * Check if a MIME type represents a vector image format.\n\t *\n\t * @param mimeType - The MIME type to check\n\t * @returns True if the MIME type is a vector image format, false otherwise\n\t * @example\n\t * ```ts\n\t * const isVector = MediaHelpers.isVectorImageType('image/svg+xml')\n\t * console.log(isVector) // true\n\t * ```\n\t * @public\n\t */\n\tstatic isVectorImageType(mimeType: string | null): boolean {\n\t\treturn DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES.includes((mimeType as any) || '')\n\t}\n\n\t/**\n\t * Check if a MIME type represents any supported image format (static, animated, or vector).\n\t *\n\t * @param mimeType - The MIME type to check\n\t * @returns True if the MIME type is a supported image format, false otherwise\n\t * @example\n\t * ```ts\n\t * const isImage = MediaHelpers.isImageType('image/png')\n\t * console.log(isImage) // true\n\t * ```\n\t * @public\n\t */\n\tstatic isImageType(mimeType: string): boolean {\n\t\treturn DEFAULT_SUPPORTED_IMAGE_TYPES.includes((mimeType as any) || '')\n\t}\n\n\t/**\n\t * Utility function to create an object URL from a blob, execute a function with it, and automatically clean it up.\n\t *\n\t * @param blob - The Blob to create an object URL for\n\t * @param fn - Function to execute with the object URL\n\t * @returns Promise that resolves to the result of the function\n\t * @example\n\t * ```ts\n\t * const result = await MediaHelpers.usingObjectURL(imageBlob, async (url) => {\n\t * const { w, h } = await MediaHelpers.getImageAndDimensions(url)\n\t * return { width: w, height: h }\n\t * })\n\t * // Object URL is automatically revoked after function completes\n\t * console.log(`Image dimensions: ${result.width}x${result.height}`)\n\t * ```\n\t * @public\n\t */\n\tstatic async usingObjectURL<T>(blob: Blob, fn: (url: string) => Promise<T>): Promise<T> {\n\t\tconst url = URL.createObjectURL(blob)\n\t\ttry {\n\t\t\treturn await fn(url)\n\t\t} finally {\n\t\t\tURL.revokeObjectURL(url)\n\t\t}\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -939,6 +939,7 @@ export declare class MediaHelpers {
939
939
  * Load a video element from a URL with cross-origin support.
940
940
  *
941
941
  * @param src - The URL of the video to load
942
+ * @param doc - Optional document to create the video element in
942
943
  * @returns Promise that resolves to the loaded HTMLVideoElement
943
944
  * @example
944
945
  * ```ts
@@ -947,7 +948,7 @@ export declare class MediaHelpers {
947
948
  * ```
948
949
  * @public
949
950
  */
950
- static loadVideo(src: string): Promise<HTMLVideoElement>;
951
+ static loadVideo(src: string, doc?: Document): Promise<HTMLVideoElement>;
951
952
  /**
952
953
  * Extract a frame from a video element as a data URL.
953
954
  *
@@ -969,6 +970,7 @@ export declare class MediaHelpers {
969
970
  * Load an image from a URL and get its dimensions along with the image element.
970
971
  *
971
972
  * @param src - The URL of the image to load
973
+ * @param doc - Optional document to use for DOM operations (e.g. measuring SVG dimensions)
972
974
  * @returns Promise that resolves to an object with width, height, and the image element
973
975
  * @example
974
976
  * ```ts
@@ -979,7 +981,7 @@ export declare class MediaHelpers {
979
981
  * ```
980
982
  * @public
981
983
  */
982
- static getImageAndDimensions(src: string): Promise<{
984
+ static getImageAndDimensions(src: string, doc?: Document): Promise<{
983
985
  h: number;
984
986
  image: HTMLImageElement;
985
987
  w: number;
@@ -988,6 +990,7 @@ export declare class MediaHelpers {
988
990
  * Get the size of a video blob
989
991
  *
990
992
  * @param blob - A Blob containing the video
993
+ * @param doc - Optional document to create elements in
991
994
  * @returns Promise that resolves to an object with width and height properties
992
995
  * @example
993
996
  * ```ts
@@ -997,7 +1000,7 @@ export declare class MediaHelpers {
997
1000
  * ```
998
1001
  * @public
999
1002
  */
1000
- static getVideoSize(blob: Blob): Promise<{
1003
+ static getVideoSize(blob: Blob, doc?: Document): Promise<{
1001
1004
  h: number;
1002
1005
  w: number;
1003
1006
  }>;
@@ -1005,6 +1008,7 @@ export declare class MediaHelpers {
1005
1008
  * Get the size of an image blob
1006
1009
  *
1007
1010
  * @param blob - A Blob containing the image
1011
+ * @param doc - Optional document to use for DOM operations
1008
1012
  * @returns Promise that resolves to an object with width and height properties
1009
1013
  * @example
1010
1014
  * ```ts
@@ -1014,7 +1018,7 @@ export declare class MediaHelpers {
1014
1018
  * ```
1015
1019
  * @public
1016
1020
  */
1017
- static getImageSize(blob: Blob): Promise<{
1021
+ static getImageSize(blob: Blob, doc?: Document): Promise<{
1018
1022
  h: number;
1019
1023
  pixelRatio: number;
1020
1024
  w: number;
@@ -101,7 +101,7 @@ import { registerTldrawLibraryVersion as registerTldrawLibraryVersion2 } from ".
101
101
  import { warnDeprecatedGetter, warnOnce } from "./lib/warn.mjs";
102
102
  registerTldrawLibraryVersion(
103
103
  "@tldraw/utils",
104
- "4.5.2",
104
+ "4.6.0-canary.b9b3bfdc07c7",
105
105
  "esm"
106
106
  );
107
107
  export {
@@ -36,6 +36,7 @@ class MediaHelpers {
36
36
  * Load a video element from a URL with cross-origin support.
37
37
  *
38
38
  * @param src - The URL of the video to load
39
+ * @param doc - Optional document to create the video element in
39
40
  * @returns Promise that resolves to the loaded HTMLVideoElement
40
41
  * @example
41
42
  * ```ts
@@ -44,9 +45,9 @@ class MediaHelpers {
44
45
  * ```
45
46
  * @public
46
47
  */
47
- static loadVideo(src) {
48
+ static loadVideo(src, doc) {
48
49
  return new Promise((resolve, reject) => {
49
- const video = document.createElement("video");
50
+ const video = (doc ?? document).createElement("video");
50
51
  video.onloadeddata = () => resolve(video);
51
52
  video.onerror = (e) => {
52
53
  console.error(e);
@@ -85,7 +86,7 @@ class MediaHelpers {
85
86
  }
86
87
  }
87
88
  if (video.readyState >= video.HAVE_CURRENT_DATA) {
88
- const canvas = document.createElement("canvas");
89
+ const canvas = (video.ownerDocument ?? document).createElement("canvas");
89
90
  canvas.width = video.videoWidth;
90
91
  canvas.height = video.videoHeight;
91
92
  const ctx = canvas.getContext("2d");
@@ -122,6 +123,7 @@ class MediaHelpers {
122
123
  * Load an image from a URL and get its dimensions along with the image element.
123
124
  *
124
125
  * @param src - The URL of the image to load
126
+ * @param doc - Optional document to use for DOM operations (e.g. measuring SVG dimensions)
125
127
  * @returns Promise that resolves to an object with width, height, and the image element
126
128
  * @example
127
129
  * ```ts
@@ -132,7 +134,7 @@ class MediaHelpers {
132
134
  * ```
133
135
  * @public
134
136
  */
135
- static getImageAndDimensions(src) {
137
+ static getImageAndDimensions(src, doc) {
136
138
  return new Promise((resolve, reject) => {
137
139
  const img = Image();
138
140
  img.onload = () => {
@@ -143,12 +145,13 @@ class MediaHelpers {
143
145
  h: img.naturalHeight
144
146
  };
145
147
  } else {
146
- document.body.appendChild(img);
148
+ const body = (doc ?? document).body;
149
+ body.appendChild(img);
147
150
  dimensions = {
148
151
  w: img.clientWidth,
149
152
  h: img.clientHeight
150
153
  };
151
- document.body.removeChild(img);
154
+ body.removeChild(img);
152
155
  }
153
156
  resolve({ ...dimensions, image: img });
154
157
  };
@@ -169,6 +172,7 @@ class MediaHelpers {
169
172
  * Get the size of a video blob
170
173
  *
171
174
  * @param blob - A Blob containing the video
175
+ * @param doc - Optional document to create elements in
172
176
  * @returns Promise that resolves to an object with width and height properties
173
177
  * @example
174
178
  * ```ts
@@ -178,9 +182,9 @@ class MediaHelpers {
178
182
  * ```
179
183
  * @public
180
184
  */
181
- static async getVideoSize(blob) {
185
+ static async getVideoSize(blob, doc) {
182
186
  return MediaHelpers.usingObjectURL(blob, async (url) => {
183
- const video = await MediaHelpers.loadVideo(url);
187
+ const video = await MediaHelpers.loadVideo(url, doc);
184
188
  return { w: video.videoWidth, h: video.videoHeight };
185
189
  });
186
190
  }
@@ -188,6 +192,7 @@ class MediaHelpers {
188
192
  * Get the size of an image blob
189
193
  *
190
194
  * @param blob - A Blob containing the image
195
+ * @param doc - Optional document to use for DOM operations
191
196
  * @returns Promise that resolves to an object with width and height properties
192
197
  * @example
193
198
  * ```ts
@@ -197,8 +202,11 @@ class MediaHelpers {
197
202
  * ```
198
203
  * @public
199
204
  */
200
- static async getImageSize(blob) {
201
- const { w, h } = await MediaHelpers.usingObjectURL(blob, MediaHelpers.getImageAndDimensions);
205
+ static async getImageSize(blob, doc) {
206
+ const { w, h } = await MediaHelpers.usingObjectURL(
207
+ blob,
208
+ (url) => MediaHelpers.getImageAndDimensions(url, doc)
209
+ );
202
210
  try {
203
211
  if (blob.type === "image/png") {
204
212
  const view = new DataView(await blob.arrayBuffer());
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/media/media.ts"],
4
- "sourcesContent": ["import { promiseWithResolve } from '../control'\nimport { Image } from '../network'\nimport { isApngAnimated } from './apng'\nimport { isAvifAnimated } from './avif'\nimport { isGifAnimated } from './gif'\nimport { PngHelpers } from './png'\nimport { isWebpAnimated } from './webp'\n\n/**\n * Array of supported vector image MIME types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES } from '@tldraw/utils'\n *\n * const isSvg = DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES.includes('image/svg+xml')\n * console.log(isSvg) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES = Object.freeze(['image/svg+xml' as const])\n/**\n * Array of supported static (non-animated) image MIME types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES } from '@tldraw/utils'\n *\n * const isStatic = DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES.includes('image/jpeg')\n * console.log(isStatic) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES = Object.freeze([\n\t'image/jpeg' as const,\n\t'image/png' as const,\n\t'image/webp' as const,\n])\n/**\n * Array of supported animated image MIME types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES } from '@tldraw/utils'\n *\n * const isAnimated = DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES.includes('image/gif')\n * console.log(isAnimated) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES = Object.freeze([\n\t'image/gif' as const,\n\t'image/apng' as const,\n\t'image/avif' as const,\n])\n/**\n * Array of all supported image MIME types, combining static, vector, and animated types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_IMAGE_TYPES } from '@tldraw/utils'\n *\n * const isSupported = DEFAULT_SUPPORTED_IMAGE_TYPES.includes('image/png')\n * console.log(isSupported) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_IMAGE_TYPES = Object.freeze([\n\t...DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES,\n\t...DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES,\n\t...DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES,\n])\n/**\n * Array of supported video MIME types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORT_VIDEO_TYPES } from '@tldraw/utils'\n *\n * const isVideo = DEFAULT_SUPPORT_VIDEO_TYPES.includes('video/mp4')\n * console.log(isVideo) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORT_VIDEO_TYPES = Object.freeze([\n\t'video/mp4' as const,\n\t'video/webm' as const,\n\t'video/quicktime' as const,\n])\n/**\n * Array of all supported media MIME types, combining images and videos.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_MEDIA_TYPES } from '@tldraw/utils'\n *\n * const isMediaFile = DEFAULT_SUPPORTED_MEDIA_TYPES.includes('video/mp4')\n * console.log(isMediaFile) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_MEDIA_TYPES = Object.freeze([\n\t...DEFAULT_SUPPORTED_IMAGE_TYPES,\n\t...DEFAULT_SUPPORT_VIDEO_TYPES,\n])\n/**\n * Comma-separated string of all supported media MIME types, useful for HTML file input accept attributes.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_MEDIA_TYPE_LIST } from '@tldraw/utils'\n *\n * // Use in HTML file input for media uploads\n * const input = document.createElement('input')\n * input.type = 'file'\n * input.accept = DEFAULT_SUPPORTED_MEDIA_TYPE_LIST\n * input.addEventListener('change', (e) => {\n * const files = (e.target as HTMLInputElement).files\n * if (files) console.log(`Selected ${files.length} file(s)`)\n * })\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_MEDIA_TYPE_LIST = DEFAULT_SUPPORTED_MEDIA_TYPES.join(',')\n\n/**\n * Helpers for media\n *\n * @public\n */\nexport class MediaHelpers {\n\t/**\n\t * Load a video element from a URL with cross-origin support.\n\t *\n\t * @param src - The URL of the video to load\n\t * @returns Promise that resolves to the loaded HTMLVideoElement\n\t * @example\n\t * ```ts\n\t * const video = await MediaHelpers.loadVideo('https://example.com/video.mp4')\n\t * console.log(`Video dimensions: ${video.videoWidth}x${video.videoHeight}`)\n\t * ```\n\t * @public\n\t */\n\tstatic loadVideo(src: string): Promise<HTMLVideoElement> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst video = document.createElement('video')\n\t\t\tvideo.onloadeddata = () => resolve(video)\n\t\t\tvideo.onerror = (e) => {\n\t\t\t\tconsole.error(e)\n\t\t\t\treject(new Error('Could not load video'))\n\t\t\t}\n\t\t\tvideo.crossOrigin = 'anonymous'\n\t\t\tvideo.src = src\n\t\t})\n\t}\n\n\t/**\n\t * Extract a frame from a video element as a data URL.\n\t *\n\t * @param video - The HTMLVideoElement to extract frame from\n\t * @param time - The time in seconds to extract the frame from (default: 0)\n\t * @returns Promise that resolves to a data URL of the video frame\n\t * @example\n\t * ```ts\n\t * const video = await MediaHelpers.loadVideo('https://example.com/video.mp4')\n\t * const frameDataUrl = await MediaHelpers.getVideoFrameAsDataUrl(video, 5.0)\n\t * // Use frameDataUrl as image thumbnail\n\t * const img = document.createElement('img')\n\t * img.src = frameDataUrl\n\t * ```\n\t * @public\n\t */\n\tstatic async getVideoFrameAsDataUrl(video: HTMLVideoElement, time = 0): Promise<string> {\n\t\tconst promise = promiseWithResolve<string>()\n\t\tlet didSetTime = false\n\n\t\tconst onReadyStateChanged = () => {\n\t\t\tif (!didSetTime) {\n\t\t\t\tif (video.readyState >= video.HAVE_METADATA) {\n\t\t\t\t\tdidSetTime = true\n\t\t\t\t\tvideo.currentTime = time\n\t\t\t\t} else {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (video.readyState >= video.HAVE_CURRENT_DATA) {\n\t\t\t\tconst canvas = document.createElement('canvas')\n\t\t\t\tcanvas.width = video.videoWidth\n\t\t\t\tcanvas.height = video.videoHeight\n\t\t\t\tconst ctx = canvas.getContext('2d')\n\t\t\t\tif (!ctx) {\n\t\t\t\t\tthrow new Error('Could not get 2d context')\n\t\t\t\t}\n\t\t\t\tctx.drawImage(video, 0, 0)\n\t\t\t\tpromise.resolve(canvas.toDataURL())\n\t\t\t}\n\t\t}\n\t\tconst onError = (e: Event) => {\n\t\t\tconsole.error(e)\n\t\t\tpromise.reject(new Error('Could not get video frame'))\n\t\t}\n\n\t\tvideo.addEventListener('loadedmetadata', onReadyStateChanged)\n\t\tvideo.addEventListener('loadeddata', onReadyStateChanged)\n\t\tvideo.addEventListener('canplay', onReadyStateChanged)\n\t\tvideo.addEventListener('seeked', onReadyStateChanged)\n\n\t\tvideo.addEventListener('error', onError)\n\t\tvideo.addEventListener('stalled', onError)\n\n\t\tonReadyStateChanged()\n\n\t\ttry {\n\t\t\treturn await promise\n\t\t} finally {\n\t\t\tvideo.removeEventListener('loadedmetadata', onReadyStateChanged)\n\t\t\tvideo.removeEventListener('loadeddata', onReadyStateChanged)\n\t\t\tvideo.removeEventListener('canplay', onReadyStateChanged)\n\t\t\tvideo.removeEventListener('seeked', onReadyStateChanged)\n\n\t\t\tvideo.removeEventListener('error', onError)\n\t\t\tvideo.removeEventListener('stalled', onError)\n\t\t}\n\t}\n\n\t/**\n\t * Load an image from a URL and get its dimensions along with the image element.\n\t *\n\t * @param src - The URL of the image to load\n\t * @returns Promise that resolves to an object with width, height, and the image element\n\t * @example\n\t * ```ts\n\t * const { w, h, image } = await MediaHelpers.getImageAndDimensions('https://example.com/image.png')\n\t * console.log(`Image size: ${w}x${h}`)\n\t * // Image is ready to use\n\t * document.body.appendChild(image)\n\t * ```\n\t * @public\n\t */\n\tstatic getImageAndDimensions(\n\t\tsrc: string\n\t): Promise<{ w: number; h: number; image: HTMLImageElement }> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst img = Image()\n\t\t\timg.onload = () => {\n\t\t\t\tlet dimensions\n\t\t\t\tif (img.naturalWidth) {\n\t\t\t\t\tdimensions = {\n\t\t\t\t\t\tw: img.naturalWidth,\n\t\t\t\t\t\th: img.naturalHeight,\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Sigh, Firefox doesn't have naturalWidth or naturalHeight for SVGs. :-/\n\t\t\t\t\t// We have to attach to dom and use clientWidth/clientHeight.\n\t\t\t\t\tdocument.body.appendChild(img)\n\t\t\t\t\tdimensions = {\n\t\t\t\t\t\tw: img.clientWidth,\n\t\t\t\t\t\th: img.clientHeight,\n\t\t\t\t\t}\n\t\t\t\t\tdocument.body.removeChild(img)\n\t\t\t\t}\n\t\t\t\tresolve({ ...dimensions, image: img })\n\t\t\t}\n\t\t\timg.onerror = (e) => {\n\t\t\t\tconsole.error(e)\n\t\t\t\treject(new Error('Could not load image'))\n\t\t\t}\n\t\t\timg.crossOrigin = 'anonymous'\n\t\t\timg.referrerPolicy = 'strict-origin-when-cross-origin'\n\t\t\timg.style.visibility = 'hidden'\n\t\t\timg.style.position = 'absolute'\n\t\t\timg.style.opacity = '0'\n\t\t\timg.style.zIndex = '-9999'\n\t\t\timg.src = src\n\t\t})\n\t}\n\n\t/**\n\t * Get the size of a video blob\n\t *\n\t * @param blob - A Blob containing the video\n\t * @returns Promise that resolves to an object with width and height properties\n\t * @example\n\t * ```ts\n\t * const file = new File([...], 'video.mp4', { type: 'video/mp4' })\n\t * const { w, h } = await MediaHelpers.getVideoSize(file)\n\t * console.log(`Video dimensions: ${w}x${h}`)\n\t * ```\n\t * @public\n\t */\n\tstatic async getVideoSize(blob: Blob): Promise<{ w: number; h: number }> {\n\t\treturn MediaHelpers.usingObjectURL(blob, async (url) => {\n\t\t\tconst video = await MediaHelpers.loadVideo(url)\n\t\t\treturn { w: video.videoWidth, h: video.videoHeight }\n\t\t})\n\t}\n\n\t/**\n\t * Get the size of an image blob\n\t *\n\t * @param blob - A Blob containing the image\n\t * @returns Promise that resolves to an object with width and height properties\n\t * @example\n\t * ```ts\n\t * const file = new File([...], 'image.png', { type: 'image/png' })\n\t * const { w, h } = await MediaHelpers.getImageSize(file)\n\t * console.log(`Image dimensions: ${w}x${h}`)\n\t * ```\n\t * @public\n\t */\n\tstatic async getImageSize(blob: Blob): Promise<{ w: number; h: number; pixelRatio: number }> {\n\t\tconst { w, h } = await MediaHelpers.usingObjectURL(blob, MediaHelpers.getImageAndDimensions)\n\n\t\ttry {\n\t\t\tif (blob.type === 'image/png') {\n\t\t\t\tconst view = new DataView(await blob.arrayBuffer())\n\t\t\t\tif (PngHelpers.isPng(view, 0)) {\n\t\t\t\t\tconst physChunk = PngHelpers.findChunk(view, 'pHYs')\n\t\t\t\t\tif (physChunk) {\n\t\t\t\t\t\tconst physData = PngHelpers.parsePhys(view, physChunk.dataOffset)\n\t\t\t\t\t\tif (physData.unit === 1 && physData.ppux === physData.ppuy) {\n\t\t\t\t\t\t\tconst dpi = Math.round(physData.ppux * 0.0254)\n\t\t\t\t\t\t\t// Try both standard baselines: Windows/web = 96, macOS = 72.\n\t\t\t\t\t\t\t// Pick whichever yields a clean integer ratio > 1.\n\t\t\t\t\t\t\tconst r96 = dpi / 96\n\t\t\t\t\t\t\tconst r72 = dpi / 72\n\t\t\t\t\t\t\tlet pixelRatio = 1\n\t\t\t\t\t\t\tif (Number.isInteger(r96) && r96 > 1) {\n\t\t\t\t\t\t\t\tpixelRatio = r96\n\t\t\t\t\t\t\t} else if (Number.isInteger(r72) && r72 > 1) {\n\t\t\t\t\t\t\t\tpixelRatio = r72\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (pixelRatio > 1) {\n\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\tw: Math.ceil(w / pixelRatio),\n\t\t\t\t\t\t\t\t\th: Math.ceil(h / pixelRatio),\n\t\t\t\t\t\t\t\t\tpixelRatio,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tconsole.error(err)\n\t\t\treturn { w, h, pixelRatio: 1 }\n\t\t}\n\t\treturn { w, h, pixelRatio: 1 }\n\t}\n\n\t/**\n\t * Check if a media file blob contains animation data.\n\t *\n\t * @param file - The Blob to check for animation\n\t * @returns Promise that resolves to true if the file is animated, false otherwise\n\t * @example\n\t * ```ts\n\t * const file = new File([...], 'animation.gif', { type: 'image/gif' })\n\t * const animated = await MediaHelpers.isAnimated(file)\n\t * console.log(animated ? 'Animated' : 'Static')\n\t * ```\n\t * @public\n\t */\n\tstatic async isAnimated(file: Blob): Promise<boolean> {\n\t\tif (file.type === 'image/gif') {\n\t\t\treturn isGifAnimated(await file.arrayBuffer())\n\t\t}\n\n\t\tif (file.type === 'image/avif') {\n\t\t\treturn isAvifAnimated(await file.arrayBuffer())\n\t\t}\n\n\t\tif (file.type === 'image/webp') {\n\t\t\treturn isWebpAnimated(await file.arrayBuffer())\n\t\t}\n\n\t\tif (file.type === 'image/apng') {\n\t\t\treturn isApngAnimated(await file.arrayBuffer())\n\t\t}\n\n\t\treturn false\n\t}\n\n\t/**\n\t * Check if a MIME type represents an animated image format.\n\t *\n\t * @param mimeType - The MIME type to check\n\t * @returns True if the MIME type is an animated image format, false otherwise\n\t * @example\n\t * ```ts\n\t * const isAnimated = MediaHelpers.isAnimatedImageType('image/gif')\n\t * console.log(isAnimated) // true\n\t * ```\n\t * @public\n\t */\n\tstatic isAnimatedImageType(mimeType: string | null): boolean {\n\t\treturn DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES.includes((mimeType as any) || '')\n\t}\n\n\t/**\n\t * Check if a MIME type represents a static (non-animated) image format.\n\t *\n\t * @param mimeType - The MIME type to check\n\t * @returns True if the MIME type is a static image format, false otherwise\n\t * @example\n\t * ```ts\n\t * const isStatic = MediaHelpers.isStaticImageType('image/jpeg')\n\t * console.log(isStatic) // true\n\t * ```\n\t * @public\n\t */\n\tstatic isStaticImageType(mimeType: string | null): boolean {\n\t\treturn DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES.includes((mimeType as any) || '')\n\t}\n\n\t/**\n\t * Check if a MIME type represents a vector image format.\n\t *\n\t * @param mimeType - The MIME type to check\n\t * @returns True if the MIME type is a vector image format, false otherwise\n\t * @example\n\t * ```ts\n\t * const isVector = MediaHelpers.isVectorImageType('image/svg+xml')\n\t * console.log(isVector) // true\n\t * ```\n\t * @public\n\t */\n\tstatic isVectorImageType(mimeType: string | null): boolean {\n\t\treturn DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES.includes((mimeType as any) || '')\n\t}\n\n\t/**\n\t * Check if a MIME type represents any supported image format (static, animated, or vector).\n\t *\n\t * @param mimeType - The MIME type to check\n\t * @returns True if the MIME type is a supported image format, false otherwise\n\t * @example\n\t * ```ts\n\t * const isImage = MediaHelpers.isImageType('image/png')\n\t * console.log(isImage) // true\n\t * ```\n\t * @public\n\t */\n\tstatic isImageType(mimeType: string): boolean {\n\t\treturn DEFAULT_SUPPORTED_IMAGE_TYPES.includes((mimeType as any) || '')\n\t}\n\n\t/**\n\t * Utility function to create an object URL from a blob, execute a function with it, and automatically clean it up.\n\t *\n\t * @param blob - The Blob to create an object URL for\n\t * @param fn - Function to execute with the object URL\n\t * @returns Promise that resolves to the result of the function\n\t * @example\n\t * ```ts\n\t * const result = await MediaHelpers.usingObjectURL(imageBlob, async (url) => {\n\t * const { w, h } = await MediaHelpers.getImageAndDimensions(url)\n\t * return { width: w, height: h }\n\t * })\n\t * // Object URL is automatically revoked after function completes\n\t * console.log(`Image dimensions: ${result.width}x${result.height}`)\n\t * ```\n\t * @public\n\t */\n\tstatic async usingObjectURL<T>(blob: Blob, fn: (url: string) => Promise<T>): Promise<T> {\n\t\tconst url = URL.createObjectURL(blob)\n\t\ttry {\n\t\t\treturn await fn(url)\n\t\t} finally {\n\t\t\tURL.revokeObjectURL(url)\n\t\t}\n\t}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { promiseWithResolve } from '../control'\nimport { Image } from '../network'\nimport { isApngAnimated } from './apng'\nimport { isAvifAnimated } from './avif'\nimport { isGifAnimated } from './gif'\nimport { PngHelpers } from './png'\nimport { isWebpAnimated } from './webp'\n\n/**\n * Array of supported vector image MIME types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES } from '@tldraw/utils'\n *\n * const isSvg = DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES.includes('image/svg+xml')\n * console.log(isSvg) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES = Object.freeze(['image/svg+xml' as const])\n/**\n * Array of supported static (non-animated) image MIME types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES } from '@tldraw/utils'\n *\n * const isStatic = DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES.includes('image/jpeg')\n * console.log(isStatic) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES = Object.freeze([\n\t'image/jpeg' as const,\n\t'image/png' as const,\n\t'image/webp' as const,\n])\n/**\n * Array of supported animated image MIME types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES } from '@tldraw/utils'\n *\n * const isAnimated = DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES.includes('image/gif')\n * console.log(isAnimated) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES = Object.freeze([\n\t'image/gif' as const,\n\t'image/apng' as const,\n\t'image/avif' as const,\n])\n/**\n * Array of all supported image MIME types, combining static, vector, and animated types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_IMAGE_TYPES } from '@tldraw/utils'\n *\n * const isSupported = DEFAULT_SUPPORTED_IMAGE_TYPES.includes('image/png')\n * console.log(isSupported) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_IMAGE_TYPES = Object.freeze([\n\t...DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES,\n\t...DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES,\n\t...DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES,\n])\n/**\n * Array of supported video MIME types.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORT_VIDEO_TYPES } from '@tldraw/utils'\n *\n * const isVideo = DEFAULT_SUPPORT_VIDEO_TYPES.includes('video/mp4')\n * console.log(isVideo) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORT_VIDEO_TYPES = Object.freeze([\n\t'video/mp4' as const,\n\t'video/webm' as const,\n\t'video/quicktime' as const,\n])\n/**\n * Array of all supported media MIME types, combining images and videos.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_MEDIA_TYPES } from '@tldraw/utils'\n *\n * const isMediaFile = DEFAULT_SUPPORTED_MEDIA_TYPES.includes('video/mp4')\n * console.log(isMediaFile) // true\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_MEDIA_TYPES = Object.freeze([\n\t...DEFAULT_SUPPORTED_IMAGE_TYPES,\n\t...DEFAULT_SUPPORT_VIDEO_TYPES,\n])\n/**\n * Comma-separated string of all supported media MIME types, useful for HTML file input accept attributes.\n *\n * @example\n * ```ts\n * import { DEFAULT_SUPPORTED_MEDIA_TYPE_LIST } from '@tldraw/utils'\n *\n * // Use in HTML file input for media uploads\n * const input = document.createElement('input')\n * input.type = 'file'\n * input.accept = DEFAULT_SUPPORTED_MEDIA_TYPE_LIST\n * input.addEventListener('change', (e) => {\n * const files = (e.target as HTMLInputElement).files\n * if (files) console.log(`Selected ${files.length} file(s)`)\n * })\n * ```\n * @public\n */\nexport const DEFAULT_SUPPORTED_MEDIA_TYPE_LIST = DEFAULT_SUPPORTED_MEDIA_TYPES.join(',')\n\n/**\n * Helpers for media\n *\n * @public\n */\nexport class MediaHelpers {\n\t/**\n\t * Load a video element from a URL with cross-origin support.\n\t *\n\t * @param src - The URL of the video to load\n\t * @param doc - Optional document to create the video element in\n\t * @returns Promise that resolves to the loaded HTMLVideoElement\n\t * @example\n\t * ```ts\n\t * const video = await MediaHelpers.loadVideo('https://example.com/video.mp4')\n\t * console.log(`Video dimensions: ${video.videoWidth}x${video.videoHeight}`)\n\t * ```\n\t * @public\n\t */\n\tstatic loadVideo(src: string, doc?: Document): Promise<HTMLVideoElement> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\t// eslint-disable-next-line no-restricted-globals\n\t\t\tconst video = (doc ?? document).createElement('video')\n\t\t\tvideo.onloadeddata = () => resolve(video)\n\t\t\tvideo.onerror = (e) => {\n\t\t\t\tconsole.error(e)\n\t\t\t\treject(new Error('Could not load video'))\n\t\t\t}\n\t\t\tvideo.crossOrigin = 'anonymous'\n\t\t\tvideo.src = src\n\t\t})\n\t}\n\n\t/**\n\t * Extract a frame from a video element as a data URL.\n\t *\n\t * @param video - The HTMLVideoElement to extract frame from\n\t * @param time - The time in seconds to extract the frame from (default: 0)\n\t * @returns Promise that resolves to a data URL of the video frame\n\t * @example\n\t * ```ts\n\t * const video = await MediaHelpers.loadVideo('https://example.com/video.mp4')\n\t * const frameDataUrl = await MediaHelpers.getVideoFrameAsDataUrl(video, 5.0)\n\t * // Use frameDataUrl as image thumbnail\n\t * const img = document.createElement('img')\n\t * img.src = frameDataUrl\n\t * ```\n\t * @public\n\t */\n\tstatic async getVideoFrameAsDataUrl(video: HTMLVideoElement, time = 0): Promise<string> {\n\t\tconst promise = promiseWithResolve<string>()\n\t\tlet didSetTime = false\n\n\t\tconst onReadyStateChanged = () => {\n\t\t\tif (!didSetTime) {\n\t\t\t\tif (video.readyState >= video.HAVE_METADATA) {\n\t\t\t\t\tdidSetTime = true\n\t\t\t\t\tvideo.currentTime = time\n\t\t\t\t} else {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (video.readyState >= video.HAVE_CURRENT_DATA) {\n\t\t\t\t// eslint-disable-next-line no-restricted-globals\n\t\t\t\tconst canvas = (video.ownerDocument ?? document).createElement('canvas')\n\t\t\t\tcanvas.width = video.videoWidth\n\t\t\t\tcanvas.height = video.videoHeight\n\t\t\t\tconst ctx = canvas.getContext('2d')\n\t\t\t\tif (!ctx) {\n\t\t\t\t\tthrow new Error('Could not get 2d context')\n\t\t\t\t}\n\t\t\t\tctx.drawImage(video, 0, 0)\n\t\t\t\tpromise.resolve(canvas.toDataURL())\n\t\t\t}\n\t\t}\n\t\tconst onError = (e: Event) => {\n\t\t\tconsole.error(e)\n\t\t\tpromise.reject(new Error('Could not get video frame'))\n\t\t}\n\n\t\tvideo.addEventListener('loadedmetadata', onReadyStateChanged)\n\t\tvideo.addEventListener('loadeddata', onReadyStateChanged)\n\t\tvideo.addEventListener('canplay', onReadyStateChanged)\n\t\tvideo.addEventListener('seeked', onReadyStateChanged)\n\n\t\tvideo.addEventListener('error', onError)\n\t\tvideo.addEventListener('stalled', onError)\n\n\t\tonReadyStateChanged()\n\n\t\ttry {\n\t\t\treturn await promise\n\t\t} finally {\n\t\t\tvideo.removeEventListener('loadedmetadata', onReadyStateChanged)\n\t\t\tvideo.removeEventListener('loadeddata', onReadyStateChanged)\n\t\t\tvideo.removeEventListener('canplay', onReadyStateChanged)\n\t\t\tvideo.removeEventListener('seeked', onReadyStateChanged)\n\n\t\t\tvideo.removeEventListener('error', onError)\n\t\t\tvideo.removeEventListener('stalled', onError)\n\t\t}\n\t}\n\n\t/**\n\t * Load an image from a URL and get its dimensions along with the image element.\n\t *\n\t * @param src - The URL of the image to load\n\t * @param doc - Optional document to use for DOM operations (e.g. measuring SVG dimensions)\n\t * @returns Promise that resolves to an object with width, height, and the image element\n\t * @example\n\t * ```ts\n\t * const { w, h, image } = await MediaHelpers.getImageAndDimensions('https://example.com/image.png')\n\t * console.log(`Image size: ${w}x${h}`)\n\t * // Image is ready to use\n\t * document.body.appendChild(image)\n\t * ```\n\t * @public\n\t */\n\tstatic getImageAndDimensions(\n\t\tsrc: string,\n\t\tdoc?: Document\n\t): Promise<{ w: number; h: number; image: HTMLImageElement }> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst img = Image()\n\t\t\timg.onload = () => {\n\t\t\t\tlet dimensions\n\t\t\t\tif (img.naturalWidth) {\n\t\t\t\t\tdimensions = {\n\t\t\t\t\t\tw: img.naturalWidth,\n\t\t\t\t\t\th: img.naturalHeight,\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Sigh, Firefox doesn't have naturalWidth or naturalHeight for SVGs. :-/\n\t\t\t\t\t// We have to attach to dom and use clientWidth/clientHeight.\n\t\t\t\t\t// eslint-disable-next-line no-restricted-globals\n\t\t\t\t\tconst body = (doc ?? document).body\n\t\t\t\t\tbody.appendChild(img)\n\t\t\t\t\tdimensions = {\n\t\t\t\t\t\tw: img.clientWidth,\n\t\t\t\t\t\th: img.clientHeight,\n\t\t\t\t\t}\n\t\t\t\t\tbody.removeChild(img)\n\t\t\t\t}\n\t\t\t\tresolve({ ...dimensions, image: img })\n\t\t\t}\n\t\t\timg.onerror = (e) => {\n\t\t\t\tconsole.error(e)\n\t\t\t\treject(new Error('Could not load image'))\n\t\t\t}\n\t\t\timg.crossOrigin = 'anonymous'\n\t\t\timg.referrerPolicy = 'strict-origin-when-cross-origin'\n\t\t\timg.style.visibility = 'hidden'\n\t\t\timg.style.position = 'absolute'\n\t\t\timg.style.opacity = '0'\n\t\t\timg.style.zIndex = '-9999'\n\t\t\timg.src = src\n\t\t})\n\t}\n\n\t/**\n\t * Get the size of a video blob\n\t *\n\t * @param blob - A Blob containing the video\n\t * @param doc - Optional document to create elements in\n\t * @returns Promise that resolves to an object with width and height properties\n\t * @example\n\t * ```ts\n\t * const file = new File([...], 'video.mp4', { type: 'video/mp4' })\n\t * const { w, h } = await MediaHelpers.getVideoSize(file)\n\t * console.log(`Video dimensions: ${w}x${h}`)\n\t * ```\n\t * @public\n\t */\n\tstatic async getVideoSize(blob: Blob, doc?: Document): Promise<{ w: number; h: number }> {\n\t\treturn MediaHelpers.usingObjectURL(blob, async (url) => {\n\t\t\tconst video = await MediaHelpers.loadVideo(url, doc)\n\t\t\treturn { w: video.videoWidth, h: video.videoHeight }\n\t\t})\n\t}\n\n\t/**\n\t * Get the size of an image blob\n\t *\n\t * @param blob - A Blob containing the image\n\t * @param doc - Optional document to use for DOM operations\n\t * @returns Promise that resolves to an object with width and height properties\n\t * @example\n\t * ```ts\n\t * const file = new File([...], 'image.png', { type: 'image/png' })\n\t * const { w, h } = await MediaHelpers.getImageSize(file)\n\t * console.log(`Image dimensions: ${w}x${h}`)\n\t * ```\n\t * @public\n\t */\n\tstatic async getImageSize(\n\t\tblob: Blob,\n\t\tdoc?: Document\n\t): Promise<{ w: number; h: number; pixelRatio: number }> {\n\t\tconst { w, h } = await MediaHelpers.usingObjectURL(blob, (url) =>\n\t\t\tMediaHelpers.getImageAndDimensions(url, doc)\n\t\t)\n\n\t\ttry {\n\t\t\tif (blob.type === 'image/png') {\n\t\t\t\tconst view = new DataView(await blob.arrayBuffer())\n\t\t\t\tif (PngHelpers.isPng(view, 0)) {\n\t\t\t\t\tconst physChunk = PngHelpers.findChunk(view, 'pHYs')\n\t\t\t\t\tif (physChunk) {\n\t\t\t\t\t\tconst physData = PngHelpers.parsePhys(view, physChunk.dataOffset)\n\t\t\t\t\t\tif (physData.unit === 1 && physData.ppux === physData.ppuy) {\n\t\t\t\t\t\t\tconst dpi = Math.round(physData.ppux * 0.0254)\n\t\t\t\t\t\t\t// Try both standard baselines: Windows/web = 96, macOS = 72.\n\t\t\t\t\t\t\t// Pick whichever yields a clean integer ratio > 1.\n\t\t\t\t\t\t\tconst r96 = dpi / 96\n\t\t\t\t\t\t\tconst r72 = dpi / 72\n\t\t\t\t\t\t\tlet pixelRatio = 1\n\t\t\t\t\t\t\tif (Number.isInteger(r96) && r96 > 1) {\n\t\t\t\t\t\t\t\tpixelRatio = r96\n\t\t\t\t\t\t\t} else if (Number.isInteger(r72) && r72 > 1) {\n\t\t\t\t\t\t\t\tpixelRatio = r72\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (pixelRatio > 1) {\n\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\tw: Math.ceil(w / pixelRatio),\n\t\t\t\t\t\t\t\t\th: Math.ceil(h / pixelRatio),\n\t\t\t\t\t\t\t\t\tpixelRatio,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tconsole.error(err)\n\t\t\treturn { w, h, pixelRatio: 1 }\n\t\t}\n\t\treturn { w, h, pixelRatio: 1 }\n\t}\n\n\t/**\n\t * Check if a media file blob contains animation data.\n\t *\n\t * @param file - The Blob to check for animation\n\t * @returns Promise that resolves to true if the file is animated, false otherwise\n\t * @example\n\t * ```ts\n\t * const file = new File([...], 'animation.gif', { type: 'image/gif' })\n\t * const animated = await MediaHelpers.isAnimated(file)\n\t * console.log(animated ? 'Animated' : 'Static')\n\t * ```\n\t * @public\n\t */\n\tstatic async isAnimated(file: Blob): Promise<boolean> {\n\t\tif (file.type === 'image/gif') {\n\t\t\treturn isGifAnimated(await file.arrayBuffer())\n\t\t}\n\n\t\tif (file.type === 'image/avif') {\n\t\t\treturn isAvifAnimated(await file.arrayBuffer())\n\t\t}\n\n\t\tif (file.type === 'image/webp') {\n\t\t\treturn isWebpAnimated(await file.arrayBuffer())\n\t\t}\n\n\t\tif (file.type === 'image/apng') {\n\t\t\treturn isApngAnimated(await file.arrayBuffer())\n\t\t}\n\n\t\treturn false\n\t}\n\n\t/**\n\t * Check if a MIME type represents an animated image format.\n\t *\n\t * @param mimeType - The MIME type to check\n\t * @returns True if the MIME type is an animated image format, false otherwise\n\t * @example\n\t * ```ts\n\t * const isAnimated = MediaHelpers.isAnimatedImageType('image/gif')\n\t * console.log(isAnimated) // true\n\t * ```\n\t * @public\n\t */\n\tstatic isAnimatedImageType(mimeType: string | null): boolean {\n\t\treturn DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES.includes((mimeType as any) || '')\n\t}\n\n\t/**\n\t * Check if a MIME type represents a static (non-animated) image format.\n\t *\n\t * @param mimeType - The MIME type to check\n\t * @returns True if the MIME type is a static image format, false otherwise\n\t * @example\n\t * ```ts\n\t * const isStatic = MediaHelpers.isStaticImageType('image/jpeg')\n\t * console.log(isStatic) // true\n\t * ```\n\t * @public\n\t */\n\tstatic isStaticImageType(mimeType: string | null): boolean {\n\t\treturn DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES.includes((mimeType as any) || '')\n\t}\n\n\t/**\n\t * Check if a MIME type represents a vector image format.\n\t *\n\t * @param mimeType - The MIME type to check\n\t * @returns True if the MIME type is a vector image format, false otherwise\n\t * @example\n\t * ```ts\n\t * const isVector = MediaHelpers.isVectorImageType('image/svg+xml')\n\t * console.log(isVector) // true\n\t * ```\n\t * @public\n\t */\n\tstatic isVectorImageType(mimeType: string | null): boolean {\n\t\treturn DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES.includes((mimeType as any) || '')\n\t}\n\n\t/**\n\t * Check if a MIME type represents any supported image format (static, animated, or vector).\n\t *\n\t * @param mimeType - The MIME type to check\n\t * @returns True if the MIME type is a supported image format, false otherwise\n\t * @example\n\t * ```ts\n\t * const isImage = MediaHelpers.isImageType('image/png')\n\t * console.log(isImage) // true\n\t * ```\n\t * @public\n\t */\n\tstatic isImageType(mimeType: string): boolean {\n\t\treturn DEFAULT_SUPPORTED_IMAGE_TYPES.includes((mimeType as any) || '')\n\t}\n\n\t/**\n\t * Utility function to create an object URL from a blob, execute a function with it, and automatically clean it up.\n\t *\n\t * @param blob - The Blob to create an object URL for\n\t * @param fn - Function to execute with the object URL\n\t * @returns Promise that resolves to the result of the function\n\t * @example\n\t * ```ts\n\t * const result = await MediaHelpers.usingObjectURL(imageBlob, async (url) => {\n\t * const { w, h } = await MediaHelpers.getImageAndDimensions(url)\n\t * return { width: w, height: h }\n\t * })\n\t * // Object URL is automatically revoked after function completes\n\t * console.log(`Image dimensions: ${result.width}x${result.height}`)\n\t * ```\n\t * @public\n\t */\n\tstatic async usingObjectURL<T>(blob: Blob, fn: (url: string) => Promise<T>): Promise<T> {\n\t\tconst url = URL.createObjectURL(blob)\n\t\ttry {\n\t\t\treturn await fn(url)\n\t\t} finally {\n\t\t\tURL.revokeObjectURL(url)\n\t\t}\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/utils",
3
3
  "description": "tldraw infinite canvas SDK (private utilities).",
4
- "version": "4.5.2",
4
+ "version": "4.6.0-canary.b9b3bfdc07c7",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -133,6 +133,7 @@ export class MediaHelpers {
133
133
  * Load a video element from a URL with cross-origin support.
134
134
  *
135
135
  * @param src - The URL of the video to load
136
+ * @param doc - Optional document to create the video element in
136
137
  * @returns Promise that resolves to the loaded HTMLVideoElement
137
138
  * @example
138
139
  * ```ts
@@ -141,9 +142,10 @@ export class MediaHelpers {
141
142
  * ```
142
143
  * @public
143
144
  */
144
- static loadVideo(src: string): Promise<HTMLVideoElement> {
145
+ static loadVideo(src: string, doc?: Document): Promise<HTMLVideoElement> {
145
146
  return new Promise((resolve, reject) => {
146
- const video = document.createElement('video')
147
+ // eslint-disable-next-line no-restricted-globals
148
+ const video = (doc ?? document).createElement('video')
147
149
  video.onloadeddata = () => resolve(video)
148
150
  video.onerror = (e) => {
149
151
  console.error(e)
@@ -185,7 +187,8 @@ export class MediaHelpers {
185
187
  }
186
188
 
187
189
  if (video.readyState >= video.HAVE_CURRENT_DATA) {
188
- const canvas = document.createElement('canvas')
190
+ // eslint-disable-next-line no-restricted-globals
191
+ const canvas = (video.ownerDocument ?? document).createElement('canvas')
189
192
  canvas.width = video.videoWidth
190
193
  canvas.height = video.videoHeight
191
194
  const ctx = canvas.getContext('2d')
@@ -228,6 +231,7 @@ export class MediaHelpers {
228
231
  * Load an image from a URL and get its dimensions along with the image element.
229
232
  *
230
233
  * @param src - The URL of the image to load
234
+ * @param doc - Optional document to use for DOM operations (e.g. measuring SVG dimensions)
231
235
  * @returns Promise that resolves to an object with width, height, and the image element
232
236
  * @example
233
237
  * ```ts
@@ -239,7 +243,8 @@ export class MediaHelpers {
239
243
  * @public
240
244
  */
241
245
  static getImageAndDimensions(
242
- src: string
246
+ src: string,
247
+ doc?: Document
243
248
  ): Promise<{ w: number; h: number; image: HTMLImageElement }> {
244
249
  return new Promise((resolve, reject) => {
245
250
  const img = Image()
@@ -253,12 +258,14 @@ export class MediaHelpers {
253
258
  } else {
254
259
  // Sigh, Firefox doesn't have naturalWidth or naturalHeight for SVGs. :-/
255
260
  // We have to attach to dom and use clientWidth/clientHeight.
256
- document.body.appendChild(img)
261
+ // eslint-disable-next-line no-restricted-globals
262
+ const body = (doc ?? document).body
263
+ body.appendChild(img)
257
264
  dimensions = {
258
265
  w: img.clientWidth,
259
266
  h: img.clientHeight,
260
267
  }
261
- document.body.removeChild(img)
268
+ body.removeChild(img)
262
269
  }
263
270
  resolve({ ...dimensions, image: img })
264
271
  }
@@ -280,6 +287,7 @@ export class MediaHelpers {
280
287
  * Get the size of a video blob
281
288
  *
282
289
  * @param blob - A Blob containing the video
290
+ * @param doc - Optional document to create elements in
283
291
  * @returns Promise that resolves to an object with width and height properties
284
292
  * @example
285
293
  * ```ts
@@ -289,9 +297,9 @@ export class MediaHelpers {
289
297
  * ```
290
298
  * @public
291
299
  */
292
- static async getVideoSize(blob: Blob): Promise<{ w: number; h: number }> {
300
+ static async getVideoSize(blob: Blob, doc?: Document): Promise<{ w: number; h: number }> {
293
301
  return MediaHelpers.usingObjectURL(blob, async (url) => {
294
- const video = await MediaHelpers.loadVideo(url)
302
+ const video = await MediaHelpers.loadVideo(url, doc)
295
303
  return { w: video.videoWidth, h: video.videoHeight }
296
304
  })
297
305
  }
@@ -300,6 +308,7 @@ export class MediaHelpers {
300
308
  * Get the size of an image blob
301
309
  *
302
310
  * @param blob - A Blob containing the image
311
+ * @param doc - Optional document to use for DOM operations
303
312
  * @returns Promise that resolves to an object with width and height properties
304
313
  * @example
305
314
  * ```ts
@@ -309,8 +318,13 @@ export class MediaHelpers {
309
318
  * ```
310
319
  * @public
311
320
  */
312
- static async getImageSize(blob: Blob): Promise<{ w: number; h: number; pixelRatio: number }> {
313
- const { w, h } = await MediaHelpers.usingObjectURL(blob, MediaHelpers.getImageAndDimensions)
321
+ static async getImageSize(
322
+ blob: Blob,
323
+ doc?: Document
324
+ ): Promise<{ w: number; h: number; pixelRatio: number }> {
325
+ const { w, h } = await MediaHelpers.usingObjectURL(blob, (url) =>
326
+ MediaHelpers.getImageAndDimensions(url, doc)
327
+ )
314
328
 
315
329
  try {
316
330
  if (blob.type === 'image/png') {
@@ -1,5 +1,3 @@
1
- /* eslint-disable no-restricted-syntax */
2
-
3
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
2
  import {
5
3
  clearLocalStorage,