@tldraw/utils 4.5.2 → 4.6.0-canary.034648e28756
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +8 -4
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/media/media.js +18 -10
- package/dist-cjs/lib/media/media.js.map +2 -2
- package/dist-esm/index.d.mts +8 -4
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/media/media.mjs +18 -10
- package/dist-esm/lib/media/media.mjs.map +2 -2
- package/package.json +1 -1
- package/src/lib/media/media.ts +24 -10
- package/src/lib/storage.test.ts +0 -2
package/dist-cjs/index.d.ts
CHANGED
|
@@ -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.
|
|
172
|
+
"4.6.0-canary.034648e28756",
|
|
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
|
|
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
|
-
|
|
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(
|
|
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": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAAmC;AACnC,qBAAsB;AACtB,kBAA+B;AAC/B,kBAA+B;AAC/B,iBAA8B;AAC9B,iBAA2B;AAC3B,kBAA+B;AAcxB,MAAM,uCAAuC,OAAO,OAAO,CAAC,eAAwB,CAAC;AAarF,MAAM,uCAAuC,OAAO,OAAO;AAAA,EACjE;AAAA,EACA;AAAA,EACA;AACD,CAAC;AAaM,MAAM,yCAAyC,OAAO,OAAO;AAAA,EACnE;AAAA,EACA;AAAA,EACA;AACD,CAAC;AAaM,MAAM,gCAAgC,OAAO,OAAO;AAAA,EAC1D,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACJ,CAAC;AAaM,MAAM,8BAA8B,OAAO,OAAO;AAAA,EACxD;AAAA,EACA;AAAA,EACA;AACD,CAAC;AAaM,MAAM,gCAAgC,OAAO,OAAO;AAAA,EAC1D,GAAG;AAAA,EACH,GAAG;AACJ,CAAC;AAmBM,MAAM,oCAAoC,8BAA8B,KAAK,GAAG;AAOhF,MAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,
|
|
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/dist-esm/index.d.mts
CHANGED
|
@@ -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-esm/index.mjs
CHANGED
|
@@ -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.
|
|
104
|
+
"4.6.0-canary.034648e28756",
|
|
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
|
|
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
|
-
|
|
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(
|
|
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": "AAAA,SAAS,0BAA0B;AACnC,SAAS,aAAa;AACtB,SAAS,sBAAsB;AAC/B,SAAS,sBAAsB;AAC/B,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,sBAAsB;AAcxB,MAAM,uCAAuC,OAAO,OAAO,CAAC,eAAwB,CAAC;AAarF,MAAM,uCAAuC,OAAO,OAAO;AAAA,EACjE;AAAA,EACA;AAAA,EACA;AACD,CAAC;AAaM,MAAM,yCAAyC,OAAO,OAAO;AAAA,EACnE;AAAA,EACA;AAAA,EACA;AACD,CAAC;AAaM,MAAM,gCAAgC,OAAO,OAAO;AAAA,EAC1D,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACJ,CAAC;AAaM,MAAM,8BAA8B,OAAO,OAAO;AAAA,EACxD;AAAA,EACA;AAAA,EACA;AACD,CAAC;AAaM,MAAM,gCAAgC,OAAO,OAAO;AAAA,EAC1D,GAAG;AAAA,EACH,GAAG;AACJ,CAAC;AAmBM,MAAM,oCAAoC,8BAA8B,KAAK,GAAG;AAOhF,MAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,
|
|
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
package/src/lib/media/media.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
313
|
-
|
|
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') {
|