tiny-essentials 1.29.0 → 1.29.2

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 (131) hide show
  1. package/changelog/1/29/1.md +11 -0
  2. package/changelog/1/29/2.md +82 -0
  3. package/dist/v1/TinyBasicsEs.min.js +1 -1
  4. package/dist/v1/TinyEssentials.min.js +1 -1
  5. package/dist/v1/TinyFetchLoader.min.js +1 -0
  6. package/dist/v1/TinyImageLoader.min.js +1 -0
  7. package/dist/v1/TinyMediaCache.min.js +1 -0
  8. package/dist/v1/TinyMediaLoader.min.js +1 -0
  9. package/dist/v1/basics/index.cjs +5 -0
  10. package/dist/v1/basics/index.d.mts +2 -1
  11. package/dist/v1/basics/index.d.mts.map +1 -1
  12. package/dist/v1/basics/index.mjs +2 -1
  13. package/dist/v1/basics/media/youtube.cjs +275 -0
  14. package/dist/v1/basics/media/youtube.d.mts +107 -0
  15. package/dist/v1/basics/media/youtube.d.mts.map +1 -0
  16. package/dist/v1/basics/media/youtube.mjs +214 -0
  17. package/dist/v1/index.cjs +13 -0
  18. package/dist/v1/index.d.mts +6 -1
  19. package/dist/v1/index.d.mts.map +1 -1
  20. package/dist/v1/index.mjs +7 -2
  21. package/dist/v1/libs/fs/plugins/OPFS/StorageBucket.cjs +0 -1
  22. package/dist/v1/libs/fs/plugins/OPFS/StorageBucket.d.mts +0 -1
  23. package/dist/v1/libs/fs/plugins/OPFS/StorageBucket.d.mts.map +1 -1
  24. package/dist/v1/libs/fs/plugins/OPFS/StorageBucket.mjs +0 -1
  25. package/dist/v1/libs/fs/plugins/OPFS/index.cjs +0 -8
  26. package/dist/v1/libs/fs/plugins/OPFS/index.d.mts +0 -8
  27. package/dist/v1/libs/fs/plugins/OPFS/index.d.mts.map +1 -1
  28. package/dist/v1/libs/fs/plugins/OPFS/index.mjs +0 -8
  29. package/dist/v1/libs/html/TinyHtml.cjs +1 -1
  30. package/dist/v1/libs/html/TinyHtml.d.mts +2 -2
  31. package/dist/v1/libs/html/TinyHtml.d.mts.map +1 -1
  32. package/dist/v1/libs/html/TinyHtml.mjs +1 -1
  33. package/dist/v1/libs/html/loader/TinyMediaLoader.cjs +1999 -0
  34. package/dist/v1/libs/html/loader/TinyMediaLoader.d.mts +999 -0
  35. package/dist/v1/libs/html/loader/TinyMediaLoader.d.mts.map +1 -0
  36. package/dist/v1/libs/html/loader/TinyMediaLoader.mjs +1852 -0
  37. package/dist/v1/libs/html/loader/beta/formats/TinyAudioLoader.cjs +77 -0
  38. package/dist/v1/libs/html/loader/beta/formats/TinyAudioLoader.d.mts +103 -0
  39. package/dist/v1/libs/html/loader/beta/formats/TinyAudioLoader.d.mts.map +1 -0
  40. package/dist/v1/libs/html/loader/beta/formats/TinyAudioLoader.mjs +67 -0
  41. package/dist/v1/libs/html/loader/beta/formats/TinyContentLoader.cjs +630 -0
  42. package/dist/v1/libs/html/loader/beta/formats/TinyContentLoader.d.mts +138 -0
  43. package/dist/v1/libs/html/loader/beta/formats/TinyContentLoader.d.mts.map +1 -0
  44. package/dist/v1/libs/html/loader/beta/formats/TinyContentLoader.mjs +576 -0
  45. package/dist/v1/libs/html/loader/beta/formats/TinyVideoLoader.cjs +70 -0
  46. package/dist/v1/libs/html/loader/beta/formats/TinyVideoLoader.d.mts +100 -0
  47. package/dist/v1/libs/html/loader/beta/formats/TinyVideoLoader.d.mts.map +1 -0
  48. package/dist/v1/libs/html/loader/beta/formats/TinyVideoLoader.mjs +60 -0
  49. package/dist/v1/libs/html/loader/beta/probe/TinyMediaProbe.cjs +946 -0
  50. package/dist/v1/libs/html/loader/beta/probe/TinyMediaProbe.d.mts +67 -0
  51. package/dist/v1/libs/html/loader/beta/probe/TinyMediaProbe.d.mts.map +1 -0
  52. package/dist/v1/libs/html/loader/beta/probe/TinyMediaProbe.mjs +896 -0
  53. package/dist/v1/libs/html/loader/formats/TinyFetchLoader.cjs +225 -0
  54. package/dist/v1/libs/html/loader/formats/TinyFetchLoader.d.mts +149 -0
  55. package/dist/v1/libs/html/loader/formats/TinyFetchLoader.d.mts.map +1 -0
  56. package/dist/v1/libs/html/loader/formats/TinyFetchLoader.mjs +197 -0
  57. package/dist/v1/libs/html/loader/formats/TinyImageLoader.cjs +1454 -0
  58. package/dist/v1/libs/html/loader/formats/TinyImageLoader.d.mts +200 -0
  59. package/dist/v1/libs/html/loader/formats/TinyImageLoader.d.mts.map +1 -0
  60. package/dist/v1/libs/html/loader/formats/TinyImageLoader.mjs +1317 -0
  61. package/dist/v1/libs/html/loader/probe/TinyImageProbe.cjs +668 -0
  62. package/dist/v1/libs/html/loader/probe/TinyImageProbe.d.mts +83 -0
  63. package/dist/v1/libs/html/loader/probe/TinyImageProbe.d.mts.map +1 -0
  64. package/dist/v1/libs/html/loader/probe/TinyImageProbe.mjs +626 -0
  65. package/dist/v1/libs/html/loader/utils/TinyBufferReader.cjs +277 -0
  66. package/dist/v1/libs/html/loader/utils/TinyBufferReader.d.mts +118 -0
  67. package/dist/v1/libs/html/loader/utils/TinyBufferReader.d.mts.map +1 -0
  68. package/dist/v1/libs/html/loader/utils/TinyBufferReader.mjs +247 -0
  69. package/dist/v1/libs/html/loader/utils/TinyIsoBox.cjs +128 -0
  70. package/dist/v1/libs/html/loader/utils/TinyIsoBox.d.mts +49 -0
  71. package/dist/v1/libs/html/loader/utils/TinyIsoBox.d.mts.map +1 -0
  72. package/dist/v1/libs/html/loader/utils/TinyIsoBox.mjs +116 -0
  73. package/dist/v1/libs/html/loader/utils/TinyMediaCache.cjs +796 -0
  74. package/dist/v1/libs/html/loader/utils/TinyMediaCache.d.mts +354 -0
  75. package/dist/v1/libs/html/loader/utils/TinyMediaCache.d.mts.map +1 -0
  76. package/dist/v1/libs/html/loader/utils/TinyMediaCache.mjs +734 -0
  77. package/dist/v1/libs/html/loader/utils/TinyMediaMirror.cjs +302 -0
  78. package/dist/v1/libs/html/loader/utils/TinyMediaMirror.d.mts +256 -0
  79. package/dist/v1/libs/html/loader/utils/TinyMediaMirror.d.mts.map +1 -0
  80. package/dist/v1/libs/html/loader/utils/TinyMediaMirror.mjs +280 -0
  81. package/dist/v1/libs/html/loader/utils/TinyMediaProgress.cjs +194 -0
  82. package/dist/v1/libs/html/loader/utils/TinyMediaProgress.d.mts +138 -0
  83. package/dist/v1/libs/html/loader/utils/TinyMediaProgress.d.mts.map +1 -0
  84. package/dist/v1/libs/html/loader/utils/TinyMediaProgress.mjs +175 -0
  85. package/dist/v1/libs/tools/TinyPkgExportValidator.cjs +359 -55
  86. package/dist/v1/libs/tools/TinyPkgExportValidator.d.mts +64 -6
  87. package/dist/v1/libs/tools/TinyPkgExportValidator.d.mts.map +1 -1
  88. package/dist/v1/libs/tools/TinyPkgExportValidator.mjs +320 -57
  89. package/dist/v1/libs/worker/engine/TinyWebWorkerEngine.cjs +0 -1
  90. package/dist/v1/libs/worker/engine/TinyWebWorkerEngine.d.mts +0 -1
  91. package/dist/v1/libs/worker/engine/TinyWebWorkerEngine.d.mts.map +1 -1
  92. package/dist/v1/libs/worker/engine/TinyWebWorkerEngine.mjs +0 -1
  93. package/dist/v1/webTemplates/bootstrap/5.3/scss/background/_background-grid.scss +15 -0
  94. package/dist/v1/webTemplates/bootstrap/5.3/scss/components/buttons/_buttons-contrast.scss +208 -0
  95. package/dist/v1/webTemplates/bootstrap/5.3/scss/components/buttons/_buttons-ghost.scss +89 -0
  96. package/dist/v1/webTemplates/bootstrap/5.3/scss/components/icon/_cube-icon.scss +65 -0
  97. package/dist/v1/webTemplates/bootstrap/5.3/scss/components/icon/_sphere-pulse.scss +39 -0
  98. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_badges.scss +90 -0
  99. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_bootstrap-overrides.scss +495 -0
  100. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_buttons.scss +390 -0
  101. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_colors.scss +213 -0
  102. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_effects.scss +153 -0
  103. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_forms-tones.scss +69 -0
  104. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_forms.scss +323 -0
  105. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_global.scss +31 -0
  106. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_links.scss +47 -0
  107. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_tables.scss +25 -0
  108. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_theme.scss +21 -0
  109. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_tokens.scss +161 -0
  110. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_utilities.scss +81 -0
  111. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_variables.scss +63 -0
  112. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/plugins/_form-tone-mixins.scss +236 -0
  113. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/plugins/_sizing.scss +68 -0
  114. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/plugins/_tone-mixins.scss +392 -0
  115. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/plugins/_typography.scss +112 -0
  116. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/plugins/background/_tiny_gradient_1.scss +131 -0
  117. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/plugins/colors/_pink.scss +47 -0
  118. package/dist/v1/webTemplates/multiformats/14.0/IPFSHasher/index.cjs +2 -2
  119. package/docs/v1/Exports.md +33 -2
  120. package/docs/v1/README.md +9 -0
  121. package/docs/v1/basics/youtube.md +204 -0
  122. package/docs/v1/libs/html/loader/TinyFetchLoader.md +114 -0
  123. package/docs/v1/libs/html/loader/TinyImageLoader.md +141 -0
  124. package/docs/v1/libs/html/loader/TinyMediaCache.md +166 -0
  125. package/docs/v1/libs/html/loader/TinyMediaLoader.md +300 -0
  126. package/docs/v1/libs/tools/TinyPkgExportValidator.md +55 -0
  127. package/docs/v1/webTemplates/ag-psd/31.0/ProcessSolidFilters.md +0 -4
  128. package/docs/v1/webTemplates/bootstrap/5.3/DeepStrap.md +177 -0
  129. package/package.json +281 -17
  130. /package/dist/v1/webTemplates/bootstrap/5.3/scss/input/{Placeholder-Colors-Fix.scss → _placeholder-colors-fix.scss} +0 -0
  131. /package/dist/v1/webTemplates/bootstrap/5.3/scss/spacing/{Spacing-Plus.scss → _spacing-plus.scss} +0 -0
@@ -0,0 +1,626 @@
1
+ /**
2
+ * @fileoverview Probes the intrinsic dimensions of an image by parsing its
3
+ * container header. For animated formats, it retains the buffer to extract
4
+ * full animation data (frames, loops, and duration) upon finalization.
5
+ *
6
+ * Supported Web formats: PNG (incl. APNG), JPEG, GIF, WebP, BMP, ICO, CUR, AVIF, and SVG.
7
+ */
8
+ import TinyBufferReader, { TinyNeedMoreData } from '../utils/TinyBufferReader.mjs';
9
+ import { findBox } from '../utils/TinyIsoBox.mjs';
10
+ /**
11
+ * Represents detailed animation information.
12
+ * @typedef {Object} AnimationProbeData
13
+ * @property {number} frames - Total number of frames.
14
+ * @property {number} loopCount - Number of times to loop (0 usually means infinite).
15
+ * @property {number} duration - Total duration of the animation in milliseconds.
16
+ */
17
+ /**
18
+ * Represents the intrinsic dimensions of an image.
19
+ * @typedef {Object} ImageProbeSize
20
+ * @property {number} width - The display width in pixels.
21
+ * @property {number} height - The display height in pixels.
22
+ * @property {AnimationProbeData} [animation] - The animation data.
23
+ */
24
+ /**
25
+ * Describes an image format and how to identify and parse its dimensions.
26
+ * @typedef {Object} ImageFormat
27
+ * @property {string} name - The format identifier.
28
+ * @property {string} mime - The canonical MIME type.
29
+ * @property {(bytes: Uint8Array) => boolean} sniff - A function that tests if the provided bytes match the format's signature.
30
+ * @property {(reader: TinyBufferReader) => ImageProbeSize | null} parse - A function that parses the container to extract image dimensions.
31
+ */
32
+ /**
33
+ * The largest header the probe buffers before giving up.
34
+ * @type {number}
35
+ */
36
+ const MAX_HEADER_BYTES = 64 * 1024;
37
+ /**
38
+ * Checks if a buffer starts with a specific byte signature at a given offset.
39
+ * @param {Uint8Array} bytes - The buffer to test.
40
+ * @param {readonly number[]} signature - The expected byte prefix.
41
+ * @param {number} [offset] - The offset at which to perform the test.
42
+ * @returns {boolean} True when the buffer starts with the signature at the specified offset.
43
+ */
44
+ function matches(bytes, signature, offset = 0) {
45
+ if (bytes.length < offset + signature.length)
46
+ return false;
47
+ return signature.every((byte, index) => bytes[offset + index] === byte);
48
+ }
49
+ /**
50
+ * Parses a PNG image to extract its dimensions and frame count.
51
+ * @param {TinyBufferReader} reader - The binary cursor used for reading.
52
+ * @returns {ImageProbeSize|null} The extracted dimensions, or null if the PNG is malformed.
53
+ */
54
+ function parsePng(reader) {
55
+ reader.seek(16);
56
+ const width = reader.uint32();
57
+ const height = reader.uint32();
58
+ return { width, height };
59
+ }
60
+ /**
61
+ * Parses a GIF image to extract its dimensions and frames.
62
+ * @param {TinyBufferReader} reader - The binary cursor used for reading.
63
+ * @returns {ImageProbeSize|null} The extracted dimensions, or null if the GIF is malformed.
64
+ */
65
+ function parseGif(reader) {
66
+ reader.seek(6);
67
+ const width = reader.uint16le();
68
+ const height = reader.uint16le();
69
+ return width > 0 && height > 0 ? { width, height } : null;
70
+ }
71
+ /**
72
+ * Parses a BMP image to extract its dimensions.
73
+ * @param {TinyBufferReader} reader - The binary cursor used for reading.
74
+ * @returns {ImageProbeSize|null} The extracted dimensions, or null if the BMP is malformed.
75
+ */
76
+ function parseBmp(reader) {
77
+ reader.seek(18);
78
+ const width = reader.int32le();
79
+ const height = Math.abs(reader.int32le());
80
+ return width > 0 && height > 0 ? { width, height } : null;
81
+ }
82
+ /**
83
+ * Parses an ICO or CUR image to extract its dimensions.
84
+ * @param {TinyBufferReader} reader - The binary cursor used for reading.
85
+ * @returns {ImageProbeSize|null} The extracted dimensions.
86
+ */
87
+ function parseIco(reader) {
88
+ reader.seek(6);
89
+ const width = reader.uint8() || 256;
90
+ const height = reader.uint8() || 256;
91
+ return { width, height };
92
+ }
93
+ /**
94
+ * Parses a JPEG image to extract its dimensions.
95
+ * @param {TinyBufferReader} reader - The binary cursor used for reading.
96
+ * @returns {ImageProbeSize|null} The extracted dimensions, or null if no SOF marker is found.
97
+ */
98
+ function parseJpeg(reader) {
99
+ const end = reader.limit;
100
+ reader.seek(2);
101
+ while (reader.offset + 4 <= end) {
102
+ if (reader.uint8() !== 0xff)
103
+ continue;
104
+ let marker = reader.uint8();
105
+ while (marker === 0xff && reader.offset < end) {
106
+ marker = reader.uint8();
107
+ }
108
+ if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd9))
109
+ continue;
110
+ if (marker === 0xda)
111
+ return null;
112
+ const length = reader.uint16();
113
+ const isSof = marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
114
+ if (isSof) {
115
+ reader.skip(1);
116
+ const height = reader.uint16();
117
+ const width = reader.uint16();
118
+ return width > 0 && height > 0 ? { width, height } : null;
119
+ }
120
+ reader.skip(length - 2);
121
+ }
122
+ return null;
123
+ }
124
+ /**
125
+ * Parses a WebP image to extract its dimensions and frames.
126
+ * @param {TinyBufferReader} reader - The binary cursor used for reading.
127
+ * @returns {ImageProbeSize|null} The extracted dimensions, or null if the format is unsupported.
128
+ */
129
+ function parseWebp(reader) {
130
+ reader.seek(12);
131
+ const tag = reader.ascii(4);
132
+ if (tag === 'VP8 ') {
133
+ reader.seek(26);
134
+ return { width: reader.uint16le() & 0x3fff, height: reader.uint16le() & 0x3fff };
135
+ }
136
+ if (tag === 'VP8L') {
137
+ reader.seek(21);
138
+ const bits = reader.uint32le();
139
+ return { width: (bits & 0x3fff) + 1, height: ((bits >> 14) & 0x3fff) + 1 };
140
+ }
141
+ if (tag === 'VP8X') {
142
+ reader.seek(24);
143
+ const width = 1 + (reader.uint8() | (reader.uint8() << 8) | (reader.uint8() << 16));
144
+ const height = 1 + (reader.uint8() | (reader.uint8() << 8) | (reader.uint8() << 16));
145
+ return { width, height };
146
+ }
147
+ return null;
148
+ }
149
+ /**
150
+ * Parses an ISO BMFF image (e.g. AVIF) to extract dimensions.
151
+ * @param {TinyBufferReader} reader - The binary cursor used for reading.
152
+ * @returns {ImageProbeSize|null} The extracted dimensions, or null if not found.
153
+ */
154
+ function parseIsoBmff(reader) {
155
+ const end = reader.limit;
156
+ const ispe = findBox(reader, 0, end, new Set(['ispe']));
157
+ if (!ispe)
158
+ return null;
159
+ reader.seek(ispe.dataStart + 4);
160
+ const width = reader.uint32();
161
+ const height = reader.uint32();
162
+ return width > 0 && height > 0 ? { width, height } : null;
163
+ }
164
+ /**
165
+ * Parses an SVG image to extract dimensions from the `<svg>` tag attributes or `viewBox`.
166
+ * @param {TinyBufferReader} reader - The binary cursor used for reading.
167
+ * @returns {ImageProbeSize|null} The extracted dimensions, or null if not found.
168
+ */
169
+ function parseSvg(reader) {
170
+ const length = Math.min(reader.limit, 8192);
171
+ reader.seek(0);
172
+ const text = new TextDecoder('utf-8').decode(reader.bytes(length));
173
+ const match = text.match(/<svg\b[^>]*>/i);
174
+ if (!match) {
175
+ if (text.includes('<svg'))
176
+ throw new TinyNeedMoreData();
177
+ return null;
178
+ }
179
+ const attrs = match[0];
180
+ /** @param {string} name */
181
+ const read = (name) => {
182
+ const found = attrs.match(new RegExp(`${name}\\s*=\\s*["']([^"']+)["']`, 'i'));
183
+ if (!found)
184
+ return 0;
185
+ return Math.round(Number.parseFloat(found[1]));
186
+ };
187
+ const width = read('width');
188
+ const height = read('height');
189
+ if (width > 0 && height > 0)
190
+ return { width, height };
191
+ const viewBox = attrs.match(/viewBox\s*=\s*["']([^"']+)["']/i);
192
+ if (viewBox) {
193
+ const parts = viewBox[1]
194
+ .trim()
195
+ .split(/[\s,]+/)
196
+ .map(Number);
197
+ if (parts.length === 4 && parts[2] > 0 && parts[3] > 0) {
198
+ return { width: Math.round(parts[2]), height: Math.round(parts[3]) };
199
+ }
200
+ }
201
+ return null;
202
+ }
203
+ /**
204
+ * Skips a GIF sub-block chain and returns the offset after its terminator.
205
+ * @param {Uint8Array} bytes - The full GIF stream.
206
+ * @param {number} offset - The first sub-block length byte.
207
+ * @returns {number} The offset after the terminator.
208
+ */
209
+ function skipGifSubBlocks(bytes, offset) {
210
+ let cursor = offset;
211
+ while (cursor < bytes.length) {
212
+ const size = bytes[cursor];
213
+ if (size === undefined)
214
+ break; // Protection against truncated/incomplete chunks
215
+ cursor += 1;
216
+ if (size === 0)
217
+ return cursor;
218
+ cursor += size;
219
+ }
220
+ return cursor;
221
+ }
222
+ /**
223
+ * Extracts comprehensive animation metadata from a GIF stream.
224
+ * @param {Uint8Array} bytes
225
+ * @returns {AnimationProbeData|null}
226
+ */
227
+ function getGifAnimation(bytes) {
228
+ if (bytes.length < 13)
229
+ return null;
230
+ let offset = 13;
231
+ const packed = bytes[10];
232
+ if (packed & 0x80)
233
+ offset += 3 * (1 << ((packed & 0x07) + 1));
234
+ let frames = 0;
235
+ let loopCount = 1;
236
+ let hasLoop = false;
237
+ let duration = 0;
238
+ let nextDelay = 100; // Browser default (100ms) if the frame doesn't provide a delay
239
+ while (offset < bytes.length) {
240
+ const block = bytes[offset];
241
+ if (block === 0x3b)
242
+ break; // Terminator (fim do GIF)
243
+ if (block === 0x21) {
244
+ const ext = bytes[offset + 1];
245
+ if (ext === 0xff && bytes[offset + 2] === 11) {
246
+ // Application Extension (Loops)
247
+ if (matches(bytes, [0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2e, 0x30], offset + 3)) {
248
+ const subSize = bytes[offset + 14];
249
+ if (subSize === 3 && bytes[offset + 15] === 1) {
250
+ loopCount = bytes[offset + 16] | (bytes[offset + 17] << 8);
251
+ hasLoop = true;
252
+ }
253
+ }
254
+ }
255
+ else if (ext === 0xf9 && bytes[offset + 2] === 4) {
256
+ // Graphic Control Extension (Delay)
257
+ let delay = bytes[offset + 4] | (bytes[offset + 5] << 8);
258
+ // Web clamping: Delays <= 1 (10ms) are forced to 10 (100ms)
259
+ if (delay <= 1)
260
+ delay = 10;
261
+ nextDelay = delay * 10;
262
+ }
263
+ offset = skipGifSubBlocks(bytes, offset + 2);
264
+ }
265
+ else if (block === 0x2c) {
266
+ // Image Descriptor (Actual frame)
267
+ frames += 1;
268
+ duration += nextDelay;
269
+ nextDelay = 100; // Reset for the next frame in case it doesn't have a GCE
270
+ // Read the packed field at the correct offset (+9)
271
+ const localPacked = bytes[offset + 9];
272
+ offset += 10; // Skip the 10 bytes of the image header
273
+ if (localPacked & 0x80) {
274
+ // Skip the Local Color Table (if it exists)
275
+ offset += 3 * (1 << ((localPacked & 0x07) + 1));
276
+ }
277
+ offset += 1; // Skip the 'LZW Minimum Code Size' byte
278
+ offset = skipGifSubBlocks(bytes, offset); // Skip the image pixels
279
+ }
280
+ else {
281
+ break; // Unknown block, avoid infinite loops
282
+ }
283
+ if (!offset || isNaN(offset))
284
+ break;
285
+ }
286
+ if (frames <= 1)
287
+ return null;
288
+ return { frames, loopCount: hasLoop ? loopCount : 1, duration };
289
+ }
290
+ /**
291
+ * Extracts comprehensive animation metadata from a WebP stream.
292
+ * @param {Uint8Array} bytes
293
+ * @returns {AnimationProbeData|null}
294
+ */
295
+ function getWebpAnimation(bytes) {
296
+ let offset = 12;
297
+ let frames = 0;
298
+ let loopCount = 1;
299
+ let duration = 0;
300
+ let isAnimated = false;
301
+ while (offset + 8 <= bytes.length) {
302
+ const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 8);
303
+ const fourcc = String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
304
+ const size = view.getUint32(4, true);
305
+ const payload = offset + 8;
306
+ if (fourcc === 'ANIM' && payload + 6 <= bytes.length) {
307
+ isAnimated = true;
308
+ loopCount = bytes[payload + 4] | (bytes[payload + 5] << 8);
309
+ }
310
+ else if (fourcc === 'ANMF' && payload + 15 <= bytes.length) {
311
+ frames += 1;
312
+ const dur = bytes[payload + 12] | (bytes[payload + 13] << 8) | (bytes[payload + 14] << 16);
313
+ duration += dur;
314
+ }
315
+ offset += 8 + size + (size % 2);
316
+ }
317
+ if (!isAnimated || frames <= 1)
318
+ return null;
319
+ return { frames, loopCount, duration };
320
+ }
321
+ /**
322
+ * Extracts comprehensive animation metadata from an APNG stream.
323
+ * @param {Uint8Array} bytes
324
+ * @returns {AnimationProbeData|null}
325
+ */
326
+ function getApngAnimation(bytes) {
327
+ let offset = 8;
328
+ let frames = 0;
329
+ let loopCount = 1;
330
+ let duration = 0;
331
+ let hasAcTL = false;
332
+ while (offset + 8 <= bytes.length) {
333
+ const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 8);
334
+ const size = view.getUint32(0);
335
+ const type = String.fromCharCode(bytes[offset + 4], bytes[offset + 5], bytes[offset + 6], bytes[offset + 7]);
336
+ const payload = offset + 8;
337
+ if (type === 'acTL' && payload + 8 <= bytes.length) {
338
+ hasAcTL = true;
339
+ const acTLView = new DataView(bytes.buffer, bytes.byteOffset + payload, 8);
340
+ frames = acTLView.getUint32(0);
341
+ loopCount = acTLView.getUint32(4);
342
+ }
343
+ else if (type === 'fcTL' && payload + 26 <= bytes.length) {
344
+ const fcTLView = new DataView(bytes.buffer, bytes.byteOffset + payload, 26);
345
+ const delayNum = fcTLView.getUint16(20);
346
+ const delayDen = fcTLView.getUint16(22) || 100;
347
+ duration += Math.round((delayNum / delayDen) * 1000);
348
+ }
349
+ else if (type === 'IEND') {
350
+ break;
351
+ }
352
+ offset += 12 + size;
353
+ }
354
+ if (!hasAcTL || frames <= 1)
355
+ return null;
356
+ return { frames, loopCount, duration };
357
+ }
358
+ /**
359
+ * Extracts comprehensive animation metadata from an AVIF (ISOBMFF) stream.
360
+ * @param {Uint8Array} bytes
361
+ * @returns {AnimationProbeData|null}
362
+ */
363
+ function getAvifAnimation(bytes) {
364
+ const reader = new TinyBufferReader(bytes);
365
+ const end = reader.limit;
366
+ // Find the main box
367
+ const moov = findBox(reader, 0, end, new Set(['moov']));
368
+ if (!moov)
369
+ return null;
370
+ // Find the movie header (animation duration)
371
+ const mvhd = findBox(reader, moov.dataStart, moov.dataEnd, new Set(['mvhd']));
372
+ let duration = 0;
373
+ if (mvhd) {
374
+ reader.seek(mvhd.dataStart);
375
+ const version = reader.uint8();
376
+ reader.skip(3);
377
+ let timescale, dur;
378
+ if (version === 1) {
379
+ reader.skip(16);
380
+ timescale = reader.uint32();
381
+ const durBig = reader.bytes(8);
382
+ dur = (durBig[4] << 24) | (durBig[5] << 16) | (durBig[6] << 8) | durBig[7];
383
+ }
384
+ else {
385
+ reader.skip(8);
386
+ timescale = reader.uint32();
387
+ dur = reader.uint32();
388
+ }
389
+ if (timescale > 0) {
390
+ duration = Math.round((dur / timescale) * 1000);
391
+ }
392
+ }
393
+ // Dive through container boxes until reaching the sample size table (stsz) to count frames
394
+ const trak = findBox(reader, moov.dataStart, moov.dataEnd, new Set(['trak']));
395
+ if (!trak)
396
+ return null;
397
+ const mdia = findBox(reader, trak.dataStart, trak.dataEnd, new Set(['mdia']));
398
+ if (!mdia)
399
+ return null;
400
+ const minf = findBox(reader, mdia.dataStart, mdia.dataEnd, new Set(['minf']));
401
+ if (!minf)
402
+ return null;
403
+ const stbl = findBox(reader, minf.dataStart, minf.dataEnd, new Set(['stbl']));
404
+ if (!stbl)
405
+ return null;
406
+ const stsz = findBox(reader, stbl.dataStart, stbl.dataEnd, new Set(['stsz']));
407
+ let frames = 0;
408
+ if (stsz) {
409
+ reader.seek(stsz.dataStart + 4);
410
+ reader.uint32(); // sample_size
411
+ frames = reader.uint32(); // sample_count
412
+ }
413
+ if (frames <= 1)
414
+ return null;
415
+ return { frames, loopCount: 0, duration };
416
+ }
417
+ /**
418
+ * The format registry for web-supported images. The order matters: the first signature that matches wins.
419
+ * @type {ReadonlyArray<ImageFormat>}
420
+ */
421
+ const FORMATS = Object.freeze([
422
+ {
423
+ name: 'png',
424
+ mime: 'image/png',
425
+ sniff: (b) => matches(b, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
426
+ parse: parsePng,
427
+ },
428
+ {
429
+ name: 'jpeg',
430
+ mime: 'image/jpeg',
431
+ sniff: (b) => matches(b, [0xff, 0xd8, 0xff]),
432
+ parse: parseJpeg,
433
+ },
434
+ {
435
+ name: 'gif',
436
+ mime: 'image/gif',
437
+ sniff: (b) => matches(b, [0x47, 0x49, 0x46, 0x38]),
438
+ parse: parseGif,
439
+ },
440
+ { name: 'bmp', mime: 'image/bmp', sniff: (b) => matches(b, [0x42, 0x4d]), parse: parseBmp },
441
+ {
442
+ name: 'webp',
443
+ mime: 'image/webp',
444
+ sniff: (b) => matches(b, [0x52, 0x49, 0x46, 0x46]) && matches(b, [0x57, 0x45, 0x42, 0x50], 8),
445
+ parse: parseWebp,
446
+ },
447
+ {
448
+ name: 'avif',
449
+ mime: 'image/avif',
450
+ sniff: (b) => matches(b, [0x66, 0x74, 0x79, 0x70], 4),
451
+ parse: parseIsoBmff,
452
+ },
453
+ {
454
+ name: 'ico',
455
+ mime: 'image/x-icon',
456
+ sniff: (b) => matches(b, [0x00, 0x00, 0x01, 0x00]),
457
+ parse: parseIco,
458
+ },
459
+ {
460
+ name: 'cur',
461
+ mime: 'image/x-icon',
462
+ sniff: (b) => matches(b, [0x00, 0x00, 0x02, 0x00]),
463
+ parse: parseIco,
464
+ },
465
+ {
466
+ name: 'svg',
467
+ mime: 'image/svg+xml',
468
+ sniff: (b) => {
469
+ const head = new TextDecoder('utf-8').decode(b.subarray(0, 256)).trimStart();
470
+ return (head.startsWith('<svg') || head.startsWith('<?xml') || head.startsWith('<!DOCTYPE svg'));
471
+ },
472
+ parse: parseSvg,
473
+ },
474
+ ]);
475
+ /**
476
+ * Reads the intrinsic dimensions and animation frames of a web image from its container header.
477
+ */
478
+ class TinyImageProbe {
479
+ /** @type {Uint8Array} */
480
+ #buffer = new Uint8Array(0);
481
+ /** @type {ImageFormat|null} */
482
+ #format = null;
483
+ /** @type {boolean} */
484
+ #resolved = false;
485
+ /** @type {boolean} */
486
+ #abandoned = false;
487
+ /** @type {number} */
488
+ #maxBytes;
489
+ /** @type {boolean} */
490
+ #animated = false;
491
+ /** @type {number} */
492
+ #width = 0;
493
+ /** @type {number} */
494
+ #height = 0;
495
+ /** @type {number} */
496
+ #lastFrames = 0;
497
+ finalize() {
498
+ if (!this.#animated || !this.#format || !this.#resolved) {
499
+ return null;
500
+ }
501
+ const animation = this.#extractAnimation();
502
+ this.#buffer = new Uint8Array(0);
503
+ return animation ? { animation } : null;
504
+ }
505
+ /**
506
+ * @param {string} [type] - The MIME type from the response headers. Unused, kept for API compatibility.
507
+ * @param {number} [maxBytes] - The maximum number of header bytes to buffer.
508
+ * @throws {RangeError} If `maxBytes` is not a positive number.
509
+ */
510
+ constructor(type = '', maxBytes = MAX_HEADER_BYTES) {
511
+ if (typeof maxBytes !== 'number' || Number.isNaN(maxBytes) || maxBytes < 1) {
512
+ throw new RangeError('The "maxBytes" argument must be a positive number.');
513
+ }
514
+ this.#maxBytes = maxBytes;
515
+ }
516
+ /**
517
+ * Internal helper to invoke the correct extractor based on the format.
518
+ */
519
+ #extractAnimation() {
520
+ const name = this.#format?.name;
521
+ if (name === 'gif')
522
+ return getGifAnimation(this.#buffer);
523
+ if (name === 'webp')
524
+ return getWebpAnimation(this.#buffer);
525
+ if (name === 'png')
526
+ return getApngAnimation(this.#buffer);
527
+ if (name === 'avif')
528
+ return getAvifAnimation(this.#buffer);
529
+ return null;
530
+ }
531
+ /**
532
+ * Appends a chunk and returns the dimensions once they are known.
533
+ * @param {Uint8Array} chunk - The bytes received so far.
534
+ * @returns {ImageProbeSize|null} The dimensions, or null when more data is needed.
535
+ * @throws {TypeError} If `chunk` is not a Uint8Array.
536
+ */
537
+ push(chunk) {
538
+ if (!(chunk instanceof Uint8Array)) {
539
+ throw new TypeError('The "chunk" argument must be a Uint8Array.');
540
+ }
541
+ if (this.#abandoned)
542
+ return null;
543
+ // If dimensions are already resolved and we know it's a static image, we discard the rest.
544
+ if (this.#resolved && !this.#animated) {
545
+ return null;
546
+ }
547
+ const merged = new Uint8Array(this.#buffer.length + chunk.length);
548
+ merged.set(this.#buffer);
549
+ merged.set(chunk, this.#buffer.length);
550
+ this.#buffer = merged;
551
+ if (!this.#format) {
552
+ this.#format = this.#detect();
553
+ if (!this.#format) {
554
+ if (this.#buffer.length > 512) {
555
+ this.#abandoned = true;
556
+ }
557
+ return null;
558
+ }
559
+ this.#animated = ['gif', 'webp', 'png', 'avif'].includes(this.#format.name);
560
+ }
561
+ // If dimensions are already resolved and the image is animated, we look for new frames.
562
+ if (this.#resolved && this.#animated) {
563
+ const anim = this.#extractAnimation();
564
+ if (anim && anim.frames > this.#lastFrames) {
565
+ this.#lastFrames = anim.frames;
566
+ // Return known dimensions + updated animation for the loader to emit
567
+ return { width: this.#width, height: this.#height, animation: anim };
568
+ }
569
+ return null;
570
+ }
571
+ // FIRST HEADER RESOLUTION:
572
+ try {
573
+ const size = this.#format.parse(new TinyBufferReader(this.#buffer));
574
+ if (size) {
575
+ this.#resolved = true;
576
+ this.#width = size.width;
577
+ this.#height = size.height;
578
+ if (this.#animated) {
579
+ const anim = this.#extractAnimation();
580
+ if (anim) {
581
+ size.animation = anim;
582
+ this.#lastFrames = anim.frames;
583
+ }
584
+ }
585
+ else {
586
+ this.#buffer = new Uint8Array(0); // Release memory immediately if static
587
+ }
588
+ return size;
589
+ }
590
+ }
591
+ catch (error) {
592
+ if (!(error instanceof TinyNeedMoreData))
593
+ throw error;
594
+ }
595
+ // Abandon reading only if the header exceeds the limit AND dimensions are not yet resolved
596
+ if (this.#buffer.length > this.#maxBytes && !this.#resolved) {
597
+ this.#abandoned = true;
598
+ this.#buffer = new Uint8Array(0);
599
+ }
600
+ return null;
601
+ }
602
+ /**
603
+ * Releases the buffered header.
604
+ * @returns {void}
605
+ */
606
+ close() {
607
+ this.#buffer = new Uint8Array(0);
608
+ this.#abandoned = true;
609
+ }
610
+ /**
611
+ * @returns {ImageFormat|null} The detected format, or null when more data is needed.
612
+ */
613
+ #detect() {
614
+ for (const format of FORMATS) {
615
+ try {
616
+ if (format.sniff(this.#buffer))
617
+ return format;
618
+ }
619
+ catch {
620
+ return null;
621
+ }
622
+ }
623
+ return null;
624
+ }
625
+ }
626
+ export default TinyImageProbe;