@tldraw/utils 4.1.0-next.b6dfe9bccde9 → 4.1.0-next.b73a0d46b63f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/dist-cjs/index.d.ts +1350 -80
  2. package/dist-cjs/index.js +5 -5
  3. package/dist-cjs/lib/ExecutionQueue.js +79 -0
  4. package/dist-cjs/lib/ExecutionQueue.js.map +2 -2
  5. package/dist-cjs/lib/PerformanceTracker.js +43 -0
  6. package/dist-cjs/lib/PerformanceTracker.js.map +2 -2
  7. package/dist-cjs/lib/array.js +3 -1
  8. package/dist-cjs/lib/array.js.map +2 -2
  9. package/dist-cjs/lib/bind.js.map +2 -2
  10. package/dist-cjs/lib/cache.js +27 -5
  11. package/dist-cjs/lib/cache.js.map +2 -2
  12. package/dist-cjs/lib/control.js +12 -0
  13. package/dist-cjs/lib/control.js.map +2 -2
  14. package/dist-cjs/lib/debounce.js.map +2 -2
  15. package/dist-cjs/lib/error.js.map +2 -2
  16. package/dist-cjs/lib/file.js +76 -11
  17. package/dist-cjs/lib/file.js.map +2 -2
  18. package/dist-cjs/lib/function.js.map +2 -2
  19. package/dist-cjs/lib/hash.js.map +2 -2
  20. package/dist-cjs/lib/id.js.map +2 -2
  21. package/dist-cjs/lib/iterable.js.map +2 -2
  22. package/dist-cjs/lib/json-value.js.map +1 -1
  23. package/dist-cjs/lib/media/apng.js.map +2 -2
  24. package/dist-cjs/lib/media/avif.js.map +2 -2
  25. package/dist-cjs/lib/media/gif.js.map +2 -2
  26. package/dist-cjs/lib/media/media.js +130 -4
  27. package/dist-cjs/lib/media/media.js.map +2 -2
  28. package/dist-cjs/lib/media/png.js +141 -0
  29. package/dist-cjs/lib/media/png.js.map +2 -2
  30. package/dist-cjs/lib/media/webp.js +1 -0
  31. package/dist-cjs/lib/media/webp.js.map +2 -2
  32. package/dist-cjs/lib/network.js.map +2 -2
  33. package/dist-cjs/lib/number.js.map +2 -2
  34. package/dist-cjs/lib/object.js +1 -1
  35. package/dist-cjs/lib/object.js.map +2 -2
  36. package/dist-cjs/lib/perf.js.map +2 -2
  37. package/dist-cjs/lib/reordering.js.map +2 -2
  38. package/dist-cjs/lib/retry.js.map +2 -2
  39. package/dist-cjs/lib/sort.js.map +2 -2
  40. package/dist-cjs/lib/storage.js.map +2 -2
  41. package/dist-cjs/lib/stringEnum.js.map +2 -2
  42. package/dist-cjs/lib/throttle.js.map +2 -2
  43. package/dist-cjs/lib/timers.js +103 -4
  44. package/dist-cjs/lib/timers.js.map +2 -2
  45. package/dist-cjs/lib/types.js.map +1 -1
  46. package/dist-cjs/lib/url.js.map +2 -2
  47. package/dist-cjs/lib/value.js.map +2 -2
  48. package/dist-cjs/lib/version.js.map +2 -2
  49. package/dist-cjs/lib/warn.js.map +2 -2
  50. package/dist-esm/index.d.mts +1350 -80
  51. package/dist-esm/index.mjs +1 -1
  52. package/dist-esm/lib/ExecutionQueue.mjs +79 -0
  53. package/dist-esm/lib/ExecutionQueue.mjs.map +2 -2
  54. package/dist-esm/lib/PerformanceTracker.mjs +43 -0
  55. package/dist-esm/lib/PerformanceTracker.mjs.map +2 -2
  56. package/dist-esm/lib/array.mjs +3 -1
  57. package/dist-esm/lib/array.mjs.map +2 -2
  58. package/dist-esm/lib/bind.mjs.map +2 -2
  59. package/dist-esm/lib/cache.mjs +27 -5
  60. package/dist-esm/lib/cache.mjs.map +2 -2
  61. package/dist-esm/lib/control.mjs +12 -0
  62. package/dist-esm/lib/control.mjs.map +2 -2
  63. package/dist-esm/lib/debounce.mjs.map +2 -2
  64. package/dist-esm/lib/error.mjs.map +2 -2
  65. package/dist-esm/lib/file.mjs +76 -11
  66. package/dist-esm/lib/file.mjs.map +2 -2
  67. package/dist-esm/lib/function.mjs.map +2 -2
  68. package/dist-esm/lib/hash.mjs.map +2 -2
  69. package/dist-esm/lib/id.mjs.map +2 -2
  70. package/dist-esm/lib/iterable.mjs.map +2 -2
  71. package/dist-esm/lib/media/apng.mjs.map +2 -2
  72. package/dist-esm/lib/media/avif.mjs.map +2 -2
  73. package/dist-esm/lib/media/gif.mjs.map +2 -2
  74. package/dist-esm/lib/media/media.mjs +130 -4
  75. package/dist-esm/lib/media/media.mjs.map +2 -2
  76. package/dist-esm/lib/media/png.mjs +141 -0
  77. package/dist-esm/lib/media/png.mjs.map +2 -2
  78. package/dist-esm/lib/media/webp.mjs +1 -0
  79. package/dist-esm/lib/media/webp.mjs.map +2 -2
  80. package/dist-esm/lib/network.mjs.map +2 -2
  81. package/dist-esm/lib/number.mjs.map +2 -2
  82. package/dist-esm/lib/object.mjs.map +2 -2
  83. package/dist-esm/lib/perf.mjs.map +2 -2
  84. package/dist-esm/lib/reordering.mjs.map +2 -2
  85. package/dist-esm/lib/retry.mjs.map +2 -2
  86. package/dist-esm/lib/sort.mjs.map +2 -2
  87. package/dist-esm/lib/storage.mjs.map +2 -2
  88. package/dist-esm/lib/stringEnum.mjs.map +2 -2
  89. package/dist-esm/lib/throttle.mjs.map +2 -2
  90. package/dist-esm/lib/timers.mjs +103 -4
  91. package/dist-esm/lib/timers.mjs.map +2 -2
  92. package/dist-esm/lib/url.mjs.map +2 -2
  93. package/dist-esm/lib/value.mjs.map +2 -2
  94. package/dist-esm/lib/version.mjs.map +2 -2
  95. package/dist-esm/lib/warn.mjs.map +2 -2
  96. package/package.json +1 -1
  97. package/src/lib/ExecutionQueue.test.ts +162 -20
  98. package/src/lib/ExecutionQueue.ts +110 -1
  99. package/src/lib/PerformanceTracker.test.ts +124 -0
  100. package/src/lib/PerformanceTracker.ts +63 -1
  101. package/src/lib/array.test.ts +263 -1
  102. package/src/lib/array.ts +183 -14
  103. package/src/lib/bind.test.ts +47 -0
  104. package/src/lib/bind.ts +69 -4
  105. package/src/lib/cache.test.ts +73 -0
  106. package/src/lib/cache.ts +47 -6
  107. package/src/lib/control.test.ts +50 -0
  108. package/src/lib/control.ts +198 -9
  109. package/src/lib/debounce.ts +28 -3
  110. package/src/lib/error.test.ts +60 -0
  111. package/src/lib/error.ts +27 -1
  112. package/src/lib/file.test.ts +49 -0
  113. package/src/lib/file.ts +117 -12
  114. package/src/lib/function.ts +11 -0
  115. package/src/lib/hash.test.ts +99 -0
  116. package/src/lib/hash.ts +69 -2
  117. package/src/lib/id.test.ts +32 -0
  118. package/src/lib/id.ts +53 -5
  119. package/src/lib/iterable.test.ts +25 -0
  120. package/src/lib/iterable.ts +4 -5
  121. package/src/lib/json-value.ts +71 -4
  122. package/src/lib/media/apng.test.ts +67 -0
  123. package/src/lib/media/apng.ts +38 -21
  124. package/src/lib/media/avif.test.ts +26 -0
  125. package/src/lib/media/avif.ts +34 -0
  126. package/src/lib/media/gif.test.ts +52 -0
  127. package/src/lib/media/gif.ts +25 -2
  128. package/src/lib/media/media.test.ts +58 -0
  129. package/src/lib/media/media.ts +220 -11
  130. package/src/lib/media/png.ts +162 -1
  131. package/src/lib/media/webp.test.ts +81 -0
  132. package/src/lib/media/webp.ts +33 -1
  133. package/src/lib/network.test.ts +38 -0
  134. package/src/lib/network.ts +6 -0
  135. package/src/lib/number.test.ts +74 -0
  136. package/src/lib/number.ts +29 -5
  137. package/src/lib/object.test.ts +236 -0
  138. package/src/lib/object.ts +194 -14
  139. package/src/lib/perf.ts +75 -3
  140. package/src/lib/reordering.test.ts +168 -0
  141. package/src/lib/reordering.ts +62 -4
  142. package/src/lib/retry.test.ts +77 -0
  143. package/src/lib/retry.ts +47 -1
  144. package/src/lib/sort.test.ts +36 -0
  145. package/src/lib/sort.ts +22 -1
  146. package/src/lib/storage.test.ts +130 -0
  147. package/src/lib/storage.tsx +54 -8
  148. package/src/lib/stringEnum.ts +20 -1
  149. package/src/lib/throttle.ts +46 -8
  150. package/src/lib/timers.test.ts +75 -0
  151. package/src/lib/timers.ts +124 -5
  152. package/src/lib/types.ts +126 -4
  153. package/src/lib/url.test.ts +44 -0
  154. package/src/lib/url.ts +40 -1
  155. package/src/lib/value.test.ts +102 -0
  156. package/src/lib/value.ts +67 -3
  157. package/src/lib/version.test.ts +494 -56
  158. package/src/lib/version.ts +36 -1
  159. package/src/lib/warn.test.ts +64 -0
  160. package/src/lib/warn.ts +43 -2
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { isGIF, isGifAnimated } from './gif'
3
+
4
+ describe('isGIF', () => {
5
+ it('should return true for valid GIF header', () => {
6
+ const buffer = new ArrayBuffer(10)
7
+ const view = new Uint8Array(buffer)
8
+
9
+ // Set GIF header "GIF"
10
+ view[0] = 71 // G
11
+ view[1] = 73 // I
12
+ view[2] = 70 // F
13
+
14
+ expect(isGIF(buffer)).toBe(true)
15
+ })
16
+
17
+ it('should return false for non-GIF header', () => {
18
+ const buffer = new ArrayBuffer(10)
19
+ const view = new Uint8Array(buffer)
20
+
21
+ // Set PNG header instead
22
+ view[0] = 137 // PNG signature
23
+ view[1] = 80 // P
24
+ view[2] = 78 // N
25
+
26
+ expect(isGIF(buffer)).toBe(false)
27
+ })
28
+
29
+ it('should return false for empty buffer', () => {
30
+ const buffer = new ArrayBuffer(0)
31
+ expect(isGIF(buffer)).toBe(false)
32
+ })
33
+ })
34
+
35
+ describe('isGifAnimated', () => {
36
+ it('should return false for non-GIF data', () => {
37
+ const buffer = new ArrayBuffer(20)
38
+ const view = new Uint8Array(buffer)
39
+
40
+ // Set non-GIF header
41
+ view[0] = 80 // P
42
+ view[1] = 78 // N
43
+ view[2] = 71 // G
44
+
45
+ expect(isGifAnimated(buffer)).toBe(false)
46
+ })
47
+
48
+ it('should return false for empty buffer', () => {
49
+ const buffer = new ArrayBuffer(0)
50
+ expect(isGifAnimated(buffer)).toBe(false)
51
+ })
52
+ })
@@ -16,8 +16,18 @@ function getDataBlocksLength(buffer: Uint8Array, offset: number): number {
16
16
  }
17
17
 
18
18
  /**
19
- * Checks if buffer contains GIF image
19
+ * Checks if buffer contains GIF image by examining the file header.
20
20
  *
21
+ * @param buffer - The ArrayBuffer containing the image data to check
22
+ * @returns True if the buffer contains a GIF image, false otherwise
23
+ * @example
24
+ * ```ts
25
+ * // Check a file from user input
26
+ * const file = event.target.files[0]
27
+ * const buffer = await file.arrayBuffer()
28
+ * const isGif = isGIF(buffer)
29
+ * console.log(isGif ? 'GIF image' : 'Not a GIF')
30
+ * ```
21
31
  * @public
22
32
  */
23
33
  export function isGIF(buffer: ArrayBuffer): boolean {
@@ -27,8 +37,21 @@ export function isGIF(buffer: ArrayBuffer): boolean {
27
37
  }
28
38
 
29
39
  /**
30
- * Checks if buffer contains animated GIF image
40
+ * Checks if buffer contains animated GIF image by parsing the GIF structure and counting image descriptors.
41
+ * A GIF is considered animated if it contains more than one image descriptor block.
31
42
  *
43
+ * @param buffer - The ArrayBuffer containing the GIF image data
44
+ * @returns True if the GIF is animated (contains multiple frames), false otherwise
45
+ * @example
46
+ * ```ts
47
+ * // Check if a GIF file is animated
48
+ * const file = event.target.files[0]
49
+ * if (file.type === 'image/gif') {
50
+ * const buffer = await file.arrayBuffer()
51
+ * const animated = isGifAnimated(buffer)
52
+ * console.log(animated ? 'Animated GIF' : 'Static GIF')
53
+ * }
54
+ * ```
32
55
  * @public
33
56
  */
34
57
  export function isGifAnimated(buffer: ArrayBuffer): boolean {
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { MediaHelpers } from './media'
3
+
4
+ describe('MediaHelpers', () => {
5
+ // Note: Most MediaHelpers methods are DOM-dependent and already tested through
6
+ // integration tests in packages that use them. The complex mocking required
7
+ // for these tests doesn't add value and makes tests brittle.
8
+
9
+ describe('type checking methods', () => {
10
+ it('should correctly identify animated image types', () => {
11
+ expect(MediaHelpers.isAnimatedImageType('image/gif')).toBe(true)
12
+ expect(MediaHelpers.isAnimatedImageType('image/apng')).toBe(true)
13
+ expect(MediaHelpers.isAnimatedImageType('image/avif')).toBe(true)
14
+ expect(MediaHelpers.isAnimatedImageType('image/webp')).toBe(false) // webp can be static
15
+ })
16
+
17
+ it('should correctly identify static image types', () => {
18
+ expect(MediaHelpers.isStaticImageType('image/jpeg')).toBe(true)
19
+ expect(MediaHelpers.isStaticImageType('image/png')).toBe(true)
20
+ expect(MediaHelpers.isStaticImageType('image/webp')).toBe(true)
21
+ expect(MediaHelpers.isStaticImageType('image/gif')).toBe(false)
22
+ })
23
+
24
+ it('should correctly identify vector image types', () => {
25
+ expect(MediaHelpers.isVectorImageType('image/svg+xml')).toBe(true)
26
+ expect(MediaHelpers.isVectorImageType('image/jpeg')).toBe(false)
27
+ expect(MediaHelpers.isVectorImageType('image/gif')).toBe(false)
28
+ })
29
+
30
+ it('should correctly identify all image types', () => {
31
+ expect(MediaHelpers.isImageType('image/jpeg')).toBe(true)
32
+ expect(MediaHelpers.isImageType('image/png')).toBe(true)
33
+ expect(MediaHelpers.isImageType('image/gif')).toBe(true)
34
+ expect(MediaHelpers.isImageType('image/svg+xml')).toBe(true)
35
+ expect(MediaHelpers.isImageType('video/mp4')).toBe(false)
36
+ expect(MediaHelpers.isImageType('text/plain')).toBe(false)
37
+ })
38
+
39
+ it('should handle null and undefined gracefully', () => {
40
+ expect(MediaHelpers.isAnimatedImageType(null)).toBe(false)
41
+ expect(MediaHelpers.isStaticImageType(null)).toBe(false)
42
+ expect(MediaHelpers.isVectorImageType(null)).toBe(false)
43
+ })
44
+ })
45
+
46
+ // Note: The following methods are not tested here as they require extensive DOM mocking
47
+ // that would test the mocking framework more than the actual business logic:
48
+ // - loadVideo: Complex DOM video element mocking
49
+ // - getVideoFrameAsDataUrl: Canvas and video interaction mocking
50
+ // - getImageAndDimensions: Image element and DOM manipulation mocking
51
+ // - getVideoSize: Depends on loadVideo
52
+ // - getImageSize: Complex PNG parsing logic with extensive mocking
53
+ // - isAnimated: Delegates to format-specific functions already tested elsewhere
54
+ // - usingObjectURL: Simple wrapper around URL APIs with cleanup
55
+
56
+ // These methods are better tested through integration tests in the consuming packages
57
+ // (like @tldraw/tldraw) where they're used in realistic scenarios.
58
+ })
@@ -6,38 +6,121 @@ import { isGifAnimated } from './gif'
6
6
  import { PngHelpers } from './png'
7
7
  import { isWebpAnimated } from './webp'
8
8
 
9
- /** @public */
9
+ /**
10
+ * Array of supported vector image MIME types.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES } from '@tldraw/utils'
15
+ *
16
+ * const isSvg = DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES.includes('image/svg+xml')
17
+ * console.log(isSvg) // true
18
+ * ```
19
+ * @public
20
+ */
10
21
  export const DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES = Object.freeze(['image/svg+xml' as const])
11
- /** @public */
22
+ /**
23
+ * Array of supported static (non-animated) image MIME types.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * import { DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES } from '@tldraw/utils'
28
+ *
29
+ * const isStatic = DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES.includes('image/jpeg')
30
+ * console.log(isStatic) // true
31
+ * ```
32
+ * @public
33
+ */
12
34
  export const DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES = Object.freeze([
13
35
  'image/jpeg' as const,
14
36
  'image/png' as const,
15
37
  'image/webp' as const,
16
38
  ])
17
- /** @public */
39
+ /**
40
+ * Array of supported animated image MIME types.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * import { DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES } from '@tldraw/utils'
45
+ *
46
+ * const isAnimated = DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES.includes('image/gif')
47
+ * console.log(isAnimated) // true
48
+ * ```
49
+ * @public
50
+ */
18
51
  export const DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES = Object.freeze([
19
52
  'image/gif' as const,
20
53
  'image/apng' as const,
21
54
  'image/avif' as const,
22
55
  ])
23
- /** @public */
56
+ /**
57
+ * Array of all supported image MIME types, combining static, vector, and animated types.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * import { DEFAULT_SUPPORTED_IMAGE_TYPES } from '@tldraw/utils'
62
+ *
63
+ * const isSupported = DEFAULT_SUPPORTED_IMAGE_TYPES.includes('image/png')
64
+ * console.log(isSupported) // true
65
+ * ```
66
+ * @public
67
+ */
24
68
  export const DEFAULT_SUPPORTED_IMAGE_TYPES = Object.freeze([
25
69
  ...DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES,
26
70
  ...DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES,
27
71
  ...DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES,
28
72
  ])
29
- /** @public */
73
+ /**
74
+ * Array of supported video MIME types.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * import { DEFAULT_SUPPORT_VIDEO_TYPES } from '@tldraw/utils'
79
+ *
80
+ * const isVideo = DEFAULT_SUPPORT_VIDEO_TYPES.includes('video/mp4')
81
+ * console.log(isVideo) // true
82
+ * ```
83
+ * @public
84
+ */
30
85
  export const DEFAULT_SUPPORT_VIDEO_TYPES = Object.freeze([
31
86
  'video/mp4' as const,
32
87
  'video/webm' as const,
33
88
  'video/quicktime' as const,
34
89
  ])
35
- /** @public */
90
+ /**
91
+ * Array of all supported media MIME types, combining images and videos.
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * import { DEFAULT_SUPPORTED_MEDIA_TYPES } from '@tldraw/utils'
96
+ *
97
+ * const isMediaFile = DEFAULT_SUPPORTED_MEDIA_TYPES.includes('video/mp4')
98
+ * console.log(isMediaFile) // true
99
+ * ```
100
+ * @public
101
+ */
36
102
  export const DEFAULT_SUPPORTED_MEDIA_TYPES = Object.freeze([
37
103
  ...DEFAULT_SUPPORTED_IMAGE_TYPES,
38
104
  ...DEFAULT_SUPPORT_VIDEO_TYPES,
39
105
  ])
40
- /** @public */
106
+ /**
107
+ * Comma-separated string of all supported media MIME types, useful for HTML file input accept attributes.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * import { DEFAULT_SUPPORTED_MEDIA_TYPE_LIST } from '@tldraw/utils'
112
+ *
113
+ * // Use in HTML file input for media uploads
114
+ * const input = document.createElement('input')
115
+ * input.type = 'file'
116
+ * input.accept = DEFAULT_SUPPORTED_MEDIA_TYPE_LIST
117
+ * input.addEventListener('change', (e) => {
118
+ * const files = (e.target as HTMLInputElement).files
119
+ * if (files) console.log(`Selected ${files.length} file(s)`)
120
+ * })
121
+ * ```
122
+ * @public
123
+ */
41
124
  export const DEFAULT_SUPPORTED_MEDIA_TYPE_LIST = DEFAULT_SUPPORTED_MEDIA_TYPES.join(',')
42
125
 
43
126
  /**
@@ -47,7 +130,15 @@ export const DEFAULT_SUPPORTED_MEDIA_TYPE_LIST = DEFAULT_SUPPORTED_MEDIA_TYPES.j
47
130
  */
48
131
  export class MediaHelpers {
49
132
  /**
50
- * Load a video from a url.
133
+ * Load a video element from a URL with cross-origin support.
134
+ *
135
+ * @param src - The URL of the video to load
136
+ * @returns Promise that resolves to the loaded HTMLVideoElement
137
+ * @example
138
+ * ```ts
139
+ * const video = await MediaHelpers.loadVideo('https://example.com/video.mp4')
140
+ * console.log(`Video dimensions: ${video.videoWidth}x${video.videoHeight}`)
141
+ * ```
51
142
  * @public
52
143
  */
53
144
  static loadVideo(src: string): Promise<HTMLVideoElement> {
@@ -63,6 +154,22 @@ export class MediaHelpers {
63
154
  })
64
155
  }
65
156
 
157
+ /**
158
+ * Extract a frame from a video element as a data URL.
159
+ *
160
+ * @param video - The HTMLVideoElement to extract frame from
161
+ * @param time - The time in seconds to extract the frame from (default: 0)
162
+ * @returns Promise that resolves to a data URL of the video frame
163
+ * @example
164
+ * ```ts
165
+ * const video = await MediaHelpers.loadVideo('https://example.com/video.mp4')
166
+ * const frameDataUrl = await MediaHelpers.getVideoFrameAsDataUrl(video, 5.0)
167
+ * // Use frameDataUrl as image thumbnail
168
+ * const img = document.createElement('img')
169
+ * img.src = frameDataUrl
170
+ * ```
171
+ * @public
172
+ */
66
173
  static async getVideoFrameAsDataUrl(video: HTMLVideoElement, time = 0): Promise<string> {
67
174
  const promise = promiseWithResolve<string>()
68
175
  let didSetTime = false
@@ -118,7 +225,17 @@ export class MediaHelpers {
118
225
  }
119
226
 
120
227
  /**
121
- * Load an image from a url.
228
+ * Load an image from a URL and get its dimensions along with the image element.
229
+ *
230
+ * @param src - The URL of the image to load
231
+ * @returns Promise that resolves to an object with width, height, and the image element
232
+ * @example
233
+ * ```ts
234
+ * const { w, h, image } = await MediaHelpers.getImageAndDimensions('https://example.com/image.png')
235
+ * console.log(`Image size: ${w}x${h}`)
236
+ * // Image is ready to use
237
+ * document.body.appendChild(image)
238
+ * ```
122
239
  * @public
123
240
  */
124
241
  static getImageAndDimensions(
@@ -162,7 +279,14 @@ export class MediaHelpers {
162
279
  /**
163
280
  * Get the size of a video blob
164
281
  *
165
- * @param blob - A SharedBlob containing the video
282
+ * @param blob - A Blob containing the video
283
+ * @returns Promise that resolves to an object with width and height properties
284
+ * @example
285
+ * ```ts
286
+ * const file = new File([...], 'video.mp4', { type: 'video/mp4' })
287
+ * const { w, h } = await MediaHelpers.getVideoSize(file)
288
+ * console.log(`Video dimensions: ${w}x${h}`)
289
+ * ```
166
290
  * @public
167
291
  */
168
292
  static async getVideoSize(blob: Blob): Promise<{ w: number; h: number }> {
@@ -175,7 +299,14 @@ export class MediaHelpers {
175
299
  /**
176
300
  * Get the size of an image blob
177
301
  *
178
- * @param blob - A Blob containing the image.
302
+ * @param blob - A Blob containing the image
303
+ * @returns Promise that resolves to an object with width and height properties
304
+ * @example
305
+ * ```ts
306
+ * const file = new File([...], 'image.png', { type: 'image/png' })
307
+ * const { w, h } = await MediaHelpers.getImageSize(file)
308
+ * console.log(`Image dimensions: ${w}x${h}`)
309
+ * ```
179
310
  * @public
180
311
  */
181
312
  static async getImageSize(blob: Blob): Promise<{ w: number; h: number }> {
@@ -210,6 +341,19 @@ export class MediaHelpers {
210
341
  return { w, h }
211
342
  }
212
343
 
344
+ /**
345
+ * Check if a media file blob contains animation data.
346
+ *
347
+ * @param file - The Blob to check for animation
348
+ * @returns Promise that resolves to true if the file is animated, false otherwise
349
+ * @example
350
+ * ```ts
351
+ * const file = new File([...], 'animation.gif', { type: 'image/gif' })
352
+ * const animated = await MediaHelpers.isAnimated(file)
353
+ * console.log(animated ? 'Animated' : 'Static')
354
+ * ```
355
+ * @public
356
+ */
213
357
  static async isAnimated(file: Blob): Promise<boolean> {
214
358
  if (file.type === 'image/gif') {
215
359
  return isGifAnimated(await file.arrayBuffer())
@@ -230,22 +374,87 @@ export class MediaHelpers {
230
374
  return false
231
375
  }
232
376
 
377
+ /**
378
+ * Check if a MIME type represents an animated image format.
379
+ *
380
+ * @param mimeType - The MIME type to check
381
+ * @returns True if the MIME type is an animated image format, false otherwise
382
+ * @example
383
+ * ```ts
384
+ * const isAnimated = MediaHelpers.isAnimatedImageType('image/gif')
385
+ * console.log(isAnimated) // true
386
+ * ```
387
+ * @public
388
+ */
233
389
  static isAnimatedImageType(mimeType: string | null): boolean {
234
390
  return DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES.includes((mimeType as any) || '')
235
391
  }
236
392
 
393
+ /**
394
+ * Check if a MIME type represents a static (non-animated) image format.
395
+ *
396
+ * @param mimeType - The MIME type to check
397
+ * @returns True if the MIME type is a static image format, false otherwise
398
+ * @example
399
+ * ```ts
400
+ * const isStatic = MediaHelpers.isStaticImageType('image/jpeg')
401
+ * console.log(isStatic) // true
402
+ * ```
403
+ * @public
404
+ */
237
405
  static isStaticImageType(mimeType: string | null): boolean {
238
406
  return DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES.includes((mimeType as any) || '')
239
407
  }
240
408
 
409
+ /**
410
+ * Check if a MIME type represents a vector image format.
411
+ *
412
+ * @param mimeType - The MIME type to check
413
+ * @returns True if the MIME type is a vector image format, false otherwise
414
+ * @example
415
+ * ```ts
416
+ * const isVector = MediaHelpers.isVectorImageType('image/svg+xml')
417
+ * console.log(isVector) // true
418
+ * ```
419
+ * @public
420
+ */
241
421
  static isVectorImageType(mimeType: string | null): boolean {
242
422
  return DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES.includes((mimeType as any) || '')
243
423
  }
244
424
 
425
+ /**
426
+ * Check if a MIME type represents any supported image format (static, animated, or vector).
427
+ *
428
+ * @param mimeType - The MIME type to check
429
+ * @returns True if the MIME type is a supported image format, false otherwise
430
+ * @example
431
+ * ```ts
432
+ * const isImage = MediaHelpers.isImageType('image/png')
433
+ * console.log(isImage) // true
434
+ * ```
435
+ * @public
436
+ */
245
437
  static isImageType(mimeType: string): boolean {
246
438
  return DEFAULT_SUPPORTED_IMAGE_TYPES.includes((mimeType as any) || '')
247
439
  }
248
440
 
441
+ /**
442
+ * Utility function to create an object URL from a blob, execute a function with it, and automatically clean it up.
443
+ *
444
+ * @param blob - The Blob to create an object URL for
445
+ * @param fn - Function to execute with the object URL
446
+ * @returns Promise that resolves to the result of the function
447
+ * @example
448
+ * ```ts
449
+ * const result = await MediaHelpers.usingObjectURL(imageBlob, async (url) => {
450
+ * const { w, h } = await MediaHelpers.getImageAndDimensions(url)
451
+ * return { width: w, height: h }
452
+ * })
453
+ * // Object URL is automatically revoked after function completes
454
+ * console.log(`Image dimensions: ${result.width}x${result.height}`)
455
+ * ```
456
+ * @public
457
+ */
249
458
  static async usingObjectURL<T>(blob: Blob, fn: (url: string) => Promise<T>): Promise<T> {
250
459
  const url = URL.createObjectURL(blob)
251
460
  try {
@@ -62,8 +62,51 @@ const crc: CRCCalculator<Uint8Array> = (current, previous) => {
62
62
  const LEN_SIZE = 4
63
63
  const CRC_SIZE = 4
64
64
 
65
- /** @public */
65
+ /**
66
+ * Utility class for reading and manipulating PNG image files.
67
+ * Provides methods for parsing PNG chunks, validating PNG format, and modifying PNG metadata.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * // Validate PNG file from blob
72
+ * const blob = new Blob([pngData], { type: 'image/png' })
73
+ * const view = new DataView(await blob.arrayBuffer())
74
+ * const isPng = PngHelpers.isPng(view, 0)
75
+ *
76
+ * // Parse PNG metadata for image processing
77
+ * const chunks = PngHelpers.readChunks(view)
78
+ * const physChunk = PngHelpers.findChunk(view, 'pHYs')
79
+ *
80
+ * // Create high-DPI PNG for export
81
+ * const highDpiBlob = PngHelpers.setPhysChunk(view, 2, { type: 'image/png' })
82
+ * ```
83
+ *
84
+ * @public
85
+ */
66
86
  export class PngHelpers {
87
+ /**
88
+ * Checks if binary data at the specified offset contains a valid PNG file signature.
89
+ * Validates the 8-byte PNG signature: 89 50 4E 47 0D 0A 1A 0A.
90
+ *
91
+ * @param view - DataView containing the binary data to check
92
+ * @param offset - Byte offset where the PNG signature should start
93
+ * @returns True if the data contains a valid PNG signature, false otherwise
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * // Validate PNG from file upload
98
+ * const file = event.target.files[0]
99
+ * const buffer = await file.arrayBuffer()
100
+ * const view = new DataView(buffer)
101
+ *
102
+ * if (PngHelpers.isPng(view, 0)) {
103
+ * console.log('Valid PNG file detected')
104
+ * // Process PNG file...
105
+ * } else {
106
+ * console.error('Not a valid PNG file')
107
+ * }
108
+ * ```
109
+ */
67
110
  static isPng(view: DataView, offset: number) {
68
111
  if (
69
112
  view.getUint8(offset + 0) === 0x89 &&
@@ -80,6 +123,25 @@ export class PngHelpers {
80
123
  return false
81
124
  }
82
125
 
126
+ /**
127
+ * Reads the 4-character chunk type identifier from a PNG chunk header.
128
+ *
129
+ * @param view - DataView containing the PNG data
130
+ * @param offset - Byte offset of the chunk type field (after length field)
131
+ * @returns 4-character string representing the chunk type (e.g., 'IHDR', 'IDAT', 'IEND')
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * // Read chunk type from PNG header (after 8-byte signature)
136
+ * const chunkType = PngHelpers.getChunkType(dataView, 8)
137
+ * console.log(chunkType) // 'IHDR' (Image Header)
138
+ *
139
+ * // Read chunk type at a specific position during parsing
140
+ * let offset = 8 // Skip PNG signature
141
+ * const chunkLength = dataView.getUint32(offset)
142
+ * const type = PngHelpers.getChunkType(dataView, offset + 4)
143
+ * ```
144
+ */
83
145
  static getChunkType(view: DataView, offset: number) {
84
146
  return [
85
147
  String.fromCharCode(view.getUint8(offset)),
@@ -89,6 +151,32 @@ export class PngHelpers {
89
151
  ].join('')
90
152
  }
91
153
 
154
+ /**
155
+ * Parses all chunks in a PNG file and returns their metadata.
156
+ * Skips duplicate IDAT chunks but includes all other chunk types.
157
+ *
158
+ * @param view - DataView containing the complete PNG file data
159
+ * @param offset - Starting byte offset (defaults to 0)
160
+ * @returns Record mapping chunk types to their metadata (start position, data offset, and size)
161
+ * @throws Error if the data is not a valid PNG file
162
+ *
163
+ * @example
164
+ * ```ts
165
+ * // Parse PNG structure for metadata extraction
166
+ * const view = new DataView(await blob.arrayBuffer())
167
+ * const chunks = PngHelpers.readChunks(view)
168
+ *
169
+ * // Check for specific chunks
170
+ * const ihdrChunk = chunks['IHDR']
171
+ * const physChunk = chunks['pHYs']
172
+ *
173
+ * if (physChunk) {
174
+ * console.log(`Found pixel density info at byte ${physChunk.start}`)
175
+ * } else {
176
+ * console.log('No pixel density information found')
177
+ * }
178
+ * ```
179
+ */
92
180
  static readChunks(view: DataView, offset = 0) {
93
181
  const chunks: Record<string, { dataOffset: number; size: number; start: number }> = {}
94
182
  if (!PngHelpers.isPng(view, offset)) {
@@ -122,6 +210,29 @@ export class PngHelpers {
122
210
  return chunks
123
211
  }
124
212
 
213
+ /**
214
+ * Parses the pHYs (physical pixel dimensions) chunk data.
215
+ * Reads pixels per unit for X and Y axes, and the unit specifier.
216
+ *
217
+ * @param view - DataView containing the PNG data
218
+ * @param offset - Byte offset of the pHYs chunk data
219
+ * @returns Object with ppux (pixels per unit X), ppuy (pixels per unit Y), and unit specifier
220
+ *
221
+ * @example
222
+ * ```ts
223
+ * // Extract pixel density information for DPI calculation
224
+ * const physChunk = PngHelpers.findChunk(dataView, 'pHYs')
225
+ * if (physChunk) {
226
+ * const physData = PngHelpers.parsePhys(dataView, physChunk.dataOffset)
227
+ *
228
+ * if (physData.unit === 1) { // meters
229
+ * const dpiX = Math.round(physData.ppux * 0.0254)
230
+ * const dpiY = Math.round(physData.ppuy * 0.0254)
231
+ * console.log(`DPI: ${dpiX} x ${dpiY}`)
232
+ * }
233
+ * }
234
+ * ```
235
+ */
125
236
  static parsePhys(view: DataView, offset: number) {
126
237
  return {
127
238
  ppux: view.getUint32(offset),
@@ -130,11 +241,61 @@ export class PngHelpers {
130
241
  }
131
242
  }
132
243
 
244
+ /**
245
+ * Finds a specific chunk type in the PNG file and returns its metadata.
246
+ *
247
+ * @param view - DataView containing the PNG file data
248
+ * @param type - 4-character chunk type to search for (e.g., 'pHYs', 'IDAT')
249
+ * @returns Chunk metadata object if found, undefined otherwise
250
+ *
251
+ * @example
252
+ * ```ts
253
+ * // Look for pixel density information in PNG
254
+ * const physChunk = PngHelpers.findChunk(dataView, 'pHYs')
255
+ * if (physChunk) {
256
+ * const physData = PngHelpers.parsePhys(dataView, physChunk.dataOffset)
257
+ * console.log(`Found pHYs chunk with ${physData.ppux} x ${physData.ppuy} pixels per unit`)
258
+ * }
259
+ *
260
+ * // Check for text metadata
261
+ * const textChunk = PngHelpers.findChunk(dataView, 'tEXt')
262
+ * if (textChunk) {
263
+ * console.log(`Found text metadata at byte ${textChunk.start}`)
264
+ * }
265
+ * ```
266
+ */
133
267
  static findChunk(view: DataView, type: string) {
134
268
  const chunks = PngHelpers.readChunks(view)
135
269
  return chunks[type]
136
270
  }
137
271
 
272
+ /**
273
+ * Adds or replaces a pHYs chunk in a PNG file to set pixel density for high-DPI displays.
274
+ * The method determines insertion point by prioritizing IDAT chunk position over existing pHYs,
275
+ * creates a properly formatted pHYs chunk with CRC validation, and returns a new Blob.
276
+ *
277
+ * @param view - DataView containing the original PNG file data
278
+ * @param dpr - Device pixel ratio multiplier (defaults to 1)
279
+ * @param options - Optional Blob constructor options for MIME type and other properties
280
+ * @returns New Blob containing the PNG with updated pixel density information
281
+ *
282
+ * @example
283
+ * ```ts
284
+ * // Export PNG with proper pixel density for high-DPI displays
285
+ * const canvas = document.createElement('canvas')
286
+ * const ctx = canvas.getContext('2d')
287
+ * // ... draw content to canvas ...
288
+ *
289
+ * canvas.toBlob(async (blob) => {
290
+ * if (blob) {
291
+ * const view = new DataView(await blob.arrayBuffer())
292
+ * // Create 2x DPI version for Retina displays
293
+ * const highDpiBlob = PngHelpers.setPhysChunk(view, 2, { type: 'image/png' })
294
+ * // Download or use the blob...
295
+ * }
296
+ * }, 'image/png')
297
+ * ```
298
+ */
138
299
  static setPhysChunk(view: DataView, dpr = 1, options?: BlobPropertyBag) {
139
300
  let offset = 46
140
301
  let size = 0