altium-toolkit 1.0.7 → 1.0.9

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 (93) hide show
  1. package/README.md +18 -6
  2. package/docs/api.md +78 -16
  3. package/docs/model-format.md +229 -8
  4. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +47 -0
  5. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
  6. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
  7. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
  8. package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
  9. package/docs/testing.md +9 -3
  10. package/package.json +1 -1
  11. package/spec/library-scope.md +7 -1
  12. package/src/core/altium/AltiumLayoutParser.mjs +104 -8
  13. package/src/core/altium/AltiumParser.mjs +191 -45
  14. package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
  15. package/src/core/altium/IntLibModelParser.mjs +240 -0
  16. package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
  17. package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
  18. package/src/core/altium/LibrarySearchIndex.mjs +215 -0
  19. package/src/core/altium/NormalizedModelSchema.mjs +36 -0
  20. package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
  21. package/src/core/altium/PcbDefaultsParser.mjs +171 -0
  22. package/src/core/altium/PcbDimensionParser.mjs +229 -0
  23. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
  24. package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
  25. package/src/core/altium/PcbLibModelParser.mjs +235 -14
  26. package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
  27. package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
  28. package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
  29. package/src/core/altium/PcbModelParser.mjs +466 -28
  30. package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
  31. package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
  32. package/src/core/altium/PcbPadStackParser.mjs +58 -0
  33. package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
  34. package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
  35. package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
  36. package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
  37. package/src/core/altium/PcbRuleParser.mjs +354 -33
  38. package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
  39. package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
  40. package/src/core/altium/PcbStatisticsBuilder.mjs +532 -0
  41. package/src/core/altium/PcbStreamExtractor.mjs +111 -4
  42. package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
  43. package/src/core/altium/PcbUnionParser.mjs +307 -0
  44. package/src/core/altium/PcbViaStackParser.mjs +98 -10
  45. package/src/core/altium/PcbViaStructureParser.mjs +335 -0
  46. package/src/core/altium/PrintableTextDecoder.mjs +53 -3
  47. package/src/core/altium/PrjPcbModelParser.mjs +257 -5
  48. package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
  49. package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
  50. package/src/core/altium/ProjectNetlistExporter.mjs +499 -0
  51. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
  52. package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
  53. package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
  54. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
  55. package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
  56. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
  57. package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
  58. package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
  59. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
  60. package/src/core/altium/SchematicHarnessParser.mjs +302 -0
  61. package/src/core/altium/SchematicImageParser.mjs +474 -3
  62. package/src/core/altium/SchematicImplementationParser.mjs +518 -0
  63. package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
  64. package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
  65. package/src/core/altium/SchematicPinParser.mjs +84 -1
  66. package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
  67. package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
  68. package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
  69. package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
  70. package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
  71. package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
  72. package/src/core/altium/SchematicTemplateParser.mjs +256 -0
  73. package/src/core/altium/SchematicTextParser.mjs +123 -0
  74. package/src/core/ole/OleCompoundDocument.mjs +20 -0
  75. package/src/parser.mjs +29 -0
  76. package/src/renderers.mjs +3 -0
  77. package/src/styles/altium-renderers.css +25 -0
  78. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  79. package/src/ui/PcbInteractionGeometry.mjs +350 -0
  80. package/src/ui/PcbInteractionIndex.mjs +593 -0
  81. package/src/ui/PcbInteractionItemRegistry.mjs +66 -0
  82. package/src/ui/PcbInteractionLayerModel.mjs +99 -0
  83. package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +74 -9
  84. package/src/ui/PcbScene3dBuilder.mjs +169 -7
  85. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  86. package/src/ui/PcbSvgRenderer.mjs +1187 -34
  87. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  88. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  89. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  90. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  91. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  92. package/src/ui/SchematicTypography.mjs +48 -5
  93. package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
@@ -6,6 +6,22 @@ import { OleCompoundDocument } from '../ole/OleCompoundDocument.mjs'
6
6
  import { ParserUtils } from './ParserUtils.mjs'
7
7
 
8
8
  const { getField, parseBoolean, parseNumericField } = ParserUtils
9
+ const BMP_HEADER_LENGTH = 54
10
+ const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]
11
+ const PNG_SCHEMA_MIME_TYPE = 'image/png'
12
+ const NATIVE_IMAGE_CLASS_MIME_TYPES = new Map([
13
+ ['TdxPNGImage', 'image/png'],
14
+ ['TPNGImage', 'image/png'],
15
+ ['TJPEGImage', 'image/jpeg'],
16
+ ['TJPGImage', 'image/jpeg'],
17
+ ['TdxJPEGImage', 'image/jpeg'],
18
+ ['TGIFImage', 'image/gif'],
19
+ ['TdxGIFImage', 'image/gif'],
20
+ ['TSVGImage', 'image/svg+xml'],
21
+ ['TdxSVGImage', 'image/svg+xml'],
22
+ ['TWebPImage', 'image/webp'],
23
+ ['TdxWebPImage', 'image/webp']
24
+ ])
9
25
 
10
26
  /**
11
27
  * Normalizes embedded and external schematic image records.
@@ -88,13 +104,24 @@ export class SchematicImageParser {
88
104
  record.recordIndex
89
105
  let mimeType = ''
90
106
  let dataBase64 = ''
107
+ let sourceMimeType = ''
108
+ let nativeClass = ''
109
+ let hasAlpha = false
91
110
  let diagnosticState = embedded ? 'missing-embedded-payload' : 'external'
92
111
 
93
112
  if (embedded && fileName && oleDocument) {
94
113
  try {
95
114
  const streamBytes = oleDocument.getStream(fileName)
96
- mimeType = SchematicImageParser.#inferMimeType(fileName)
97
- dataBase64 = SchematicImageParser.#encodeBase64(streamBytes)
115
+ const decoded =
116
+ SchematicImageParser.#decodeEmbeddedImagePayload(
117
+ streamBytes,
118
+ fileName
119
+ )
120
+ mimeType = decoded.mimeType
121
+ sourceMimeType = decoded.sourceMimeType
122
+ nativeClass = decoded.nativeClass
123
+ hasAlpha = decoded.hasAlpha
124
+ dataBase64 = SchematicImageParser.#encodeBase64(decoded.bytes)
98
125
  diagnosticState = 'embedded'
99
126
  } catch {
100
127
  diagnostics.push({
@@ -115,7 +142,7 @@ export class SchematicImageParser {
115
142
  })
116
143
  }
117
144
 
118
- return {
145
+ const image = {
119
146
  x,
120
147
  y,
121
148
  cornerX,
@@ -128,6 +155,399 @@ export class SchematicImageParser {
128
155
  renderOrder,
129
156
  diagnosticState
130
157
  }
158
+
159
+ if (sourceMimeType && sourceMimeType !== mimeType) {
160
+ image.sourceMimeType = sourceMimeType
161
+ }
162
+ if (nativeClass) {
163
+ image.nativeClass = nativeClass
164
+ }
165
+ if (hasAlpha) {
166
+ image.hasAlpha = true
167
+ }
168
+
169
+ return image
170
+ }
171
+
172
+ /**
173
+ * Chooses the browser-facing image payload from one embedded stream.
174
+ * @param {Uint8Array} bytes Embedded image stream bytes.
175
+ * @param {string} fileName Image file name from the schematic record.
176
+ * @returns {{ bytes: Uint8Array, mimeType: string, sourceMimeType: string, nativeClass: string, hasAlpha: boolean }}
177
+ */
178
+ static #decodeEmbeddedImagePayload(bytes, fileName) {
179
+ const sourceMimeType =
180
+ SchematicImageParser.#inferMimeType(fileName) ||
181
+ SchematicImageParser.#detectMimeType(bytes)
182
+ const bmpInfo = SchematicImageParser.#parseBmpInfo(bytes)
183
+ const nativePayload = SchematicImageParser.#extractNativePayload(
184
+ bytes,
185
+ bmpInfo
186
+ )
187
+
188
+ if (nativePayload) {
189
+ return {
190
+ bytes: nativePayload.bytes,
191
+ mimeType: nativePayload.mimeType,
192
+ sourceMimeType,
193
+ nativeClass: nativePayload.nativeClass,
194
+ hasAlpha: false
195
+ }
196
+ }
197
+
198
+ if (
199
+ bmpInfo &&
200
+ SchematicImageParser.#bmpHasMeaningfulAlpha(bytes, bmpInfo)
201
+ ) {
202
+ const rgba = SchematicImageParser.#decodeBmpRgba(bytes, bmpInfo)
203
+
204
+ return {
205
+ bytes: SchematicImageParser.#encodePngRgba(
206
+ bmpInfo.width,
207
+ bmpInfo.height,
208
+ rgba
209
+ ),
210
+ mimeType: PNG_SCHEMA_MIME_TYPE,
211
+ sourceMimeType: sourceMimeType || 'image/bmp',
212
+ nativeClass: '',
213
+ hasAlpha: true
214
+ }
215
+ }
216
+
217
+ return {
218
+ bytes,
219
+ mimeType: sourceMimeType,
220
+ sourceMimeType: '',
221
+ nativeClass: '',
222
+ hasAlpha: false
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Extracts a native image payload following a BMP preview when present.
228
+ * @param {Uint8Array} bytes Embedded image stream bytes.
229
+ * @param {{ fileSize: number } | null} bmpInfo Parsed BMP preview info.
230
+ * @returns {{ nativeClass: string, mimeType: string, bytes: Uint8Array } | null}
231
+ */
232
+ static #extractNativePayload(bytes, bmpInfo) {
233
+ const previewLength = Number(bmpInfo?.fileSize || 0)
234
+ if (
235
+ !Number.isInteger(previewLength) ||
236
+ previewLength <= 0 ||
237
+ previewLength + 2 >= bytes.length
238
+ ) {
239
+ return null
240
+ }
241
+
242
+ const classLength = Number(bytes[previewLength])
243
+ const nativeStart = previewLength + 1 + classLength
244
+ if (
245
+ classLength <= 0 ||
246
+ nativeStart >= bytes.length ||
247
+ nativeStart > bytes.length
248
+ ) {
249
+ return null
250
+ }
251
+
252
+ const nativeClass = new TextDecoder('windows-1252').decode(
253
+ bytes.slice(previewLength + 1, nativeStart)
254
+ )
255
+ const nativeBytes = bytes.slice(nativeStart)
256
+ const mimeType =
257
+ NATIVE_IMAGE_CLASS_MIME_TYPES.get(nativeClass) ||
258
+ SchematicImageParser.#detectMimeType(nativeBytes)
259
+
260
+ if (!mimeType || !nativeBytes.length) {
261
+ return null
262
+ }
263
+
264
+ return { nativeClass, mimeType, bytes: nativeBytes }
265
+ }
266
+
267
+ /**
268
+ * Parses basic uncompressed BMP metadata.
269
+ * @param {Uint8Array} bytes Image bytes.
270
+ * @returns {{ width: number, height: number, topDown: boolean, bitsPerPixel: number, pixelOffset: number, rowStride: number, fileSize: number } | null}
271
+ */
272
+ static #parseBmpInfo(bytes) {
273
+ if (
274
+ !bytes ||
275
+ bytes.length < BMP_HEADER_LENGTH ||
276
+ bytes[0] !== 0x42 ||
277
+ bytes[1] !== 0x4d
278
+ ) {
279
+ return null
280
+ }
281
+
282
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length)
283
+ const fileSize = view.getUint32(2, true) || bytes.length
284
+ const pixelOffset = view.getUint32(10, true)
285
+ const dibHeaderSize = view.getUint32(14, true)
286
+ const width = view.getInt32(18, true)
287
+ const rawHeight = view.getInt32(22, true)
288
+ const planes = view.getUint16(26, true)
289
+ const bitsPerPixel = view.getUint16(28, true)
290
+ const compression = view.getUint32(30, true)
291
+
292
+ if (
293
+ dibHeaderSize < 40 ||
294
+ width <= 0 ||
295
+ rawHeight === 0 ||
296
+ planes !== 1 ||
297
+ compression !== 0 ||
298
+ ![24, 32].includes(bitsPerPixel)
299
+ ) {
300
+ return null
301
+ }
302
+
303
+ const height = Math.abs(rawHeight)
304
+ const rowStride = Math.ceil((width * (bitsPerPixel / 8)) / 4) * 4
305
+ if (pixelOffset + rowStride * height > bytes.length) {
306
+ return null
307
+ }
308
+
309
+ return {
310
+ width,
311
+ height,
312
+ topDown: rawHeight < 0,
313
+ bitsPerPixel,
314
+ pixelOffset,
315
+ rowStride,
316
+ fileSize: Math.min(fileSize, bytes.length)
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Checks whether a 32-bit BMP contains usable alpha-channel data.
322
+ * @param {Uint8Array} bytes BMP bytes.
323
+ * @param {{ width: number, height: number, bitsPerPixel: number, pixelOffset: number, rowStride: number }} bmpInfo Parsed BMP info.
324
+ * @returns {boolean}
325
+ */
326
+ static #bmpHasMeaningfulAlpha(bytes, bmpInfo) {
327
+ if (bmpInfo.bitsPerPixel !== 32) {
328
+ return false
329
+ }
330
+
331
+ let hasTransparentPixel = false
332
+ let hasVisiblePixel = false
333
+
334
+ for (let y = 0; y < bmpInfo.height; y += 1) {
335
+ const rowOffset = bmpInfo.pixelOffset + y * bmpInfo.rowStride
336
+ for (let x = 0; x < bmpInfo.width; x += 1) {
337
+ const alpha = bytes[rowOffset + x * 4 + 3]
338
+ if (alpha < 255) {
339
+ hasTransparentPixel = true
340
+ }
341
+ if (alpha > 0) {
342
+ hasVisiblePixel = true
343
+ }
344
+ }
345
+ }
346
+
347
+ return hasTransparentPixel && hasVisiblePixel
348
+ }
349
+
350
+ /**
351
+ * Converts an uncompressed BMP payload into top-down RGBA pixels.
352
+ * @param {Uint8Array} bytes BMP bytes.
353
+ * @param {{ width: number, height: number, topDown: boolean, bitsPerPixel: number, pixelOffset: number, rowStride: number }} bmpInfo Parsed BMP info.
354
+ * @returns {Uint8Array}
355
+ */
356
+ static #decodeBmpRgba(bytes, bmpInfo) {
357
+ const rgba = new Uint8Array(bmpInfo.width * bmpInfo.height * 4)
358
+ const bytesPerPixel = bmpInfo.bitsPerPixel / 8
359
+
360
+ for (let y = 0; y < bmpInfo.height; y += 1) {
361
+ const sourceRow = bmpInfo.topDown ? y : bmpInfo.height - y - 1
362
+ const sourceOffset =
363
+ bmpInfo.pixelOffset + sourceRow * bmpInfo.rowStride
364
+ const targetOffset = y * bmpInfo.width * 4
365
+
366
+ for (let x = 0; x < bmpInfo.width; x += 1) {
367
+ const source = sourceOffset + x * bytesPerPixel
368
+ const target = targetOffset + x * 4
369
+ rgba[target] = bytes[source + 2]
370
+ rgba[target + 1] = bytes[source + 1]
371
+ rgba[target + 2] = bytes[source]
372
+ rgba[target + 3] =
373
+ bmpInfo.bitsPerPixel === 32 ? bytes[source + 3] : 255
374
+ }
375
+ }
376
+
377
+ return rgba
378
+ }
379
+
380
+ /**
381
+ * Encodes RGBA pixels into a minimal PNG payload.
382
+ * @param {number} width Pixel width.
383
+ * @param {number} height Pixel height.
384
+ * @param {Uint8Array} rgba Top-down RGBA pixels.
385
+ * @returns {Uint8Array}
386
+ */
387
+ static #encodePngRgba(width, height, rgba) {
388
+ const scanlineLength = width * 4 + 1
389
+ const raw = new Uint8Array(scanlineLength * height)
390
+
391
+ for (let y = 0; y < height; y += 1) {
392
+ const rowOffset = y * scanlineLength
393
+ raw[rowOffset] = 0
394
+ raw.set(
395
+ rgba.slice(y * width * 4, (y + 1) * width * 4),
396
+ rowOffset + 1
397
+ )
398
+ }
399
+
400
+ return SchematicImageParser.#concatByteArrays([
401
+ Uint8Array.from(PNG_SIGNATURE),
402
+ SchematicImageParser.#pngChunk(
403
+ 'IHDR',
404
+ SchematicImageParser.#pngHeader(width, height)
405
+ ),
406
+ SchematicImageParser.#pngChunk(
407
+ 'IDAT',
408
+ SchematicImageParser.#zlibStore(raw)
409
+ ),
410
+ SchematicImageParser.#pngChunk('IEND', new Uint8Array())
411
+ ])
412
+ }
413
+
414
+ /**
415
+ * Builds PNG IHDR chunk data.
416
+ * @param {number} width Pixel width.
417
+ * @param {number} height Pixel height.
418
+ * @returns {Uint8Array}
419
+ */
420
+ static #pngHeader(width, height) {
421
+ const header = new Uint8Array(13)
422
+ const view = new DataView(header.buffer)
423
+
424
+ view.setUint32(0, width, false)
425
+ view.setUint32(4, height, false)
426
+ header[8] = 8
427
+ header[9] = 6
428
+ header[10] = 0
429
+ header[11] = 0
430
+ header[12] = 0
431
+
432
+ return header
433
+ }
434
+
435
+ /**
436
+ * Builds a PNG chunk.
437
+ * @param {string} type Four-byte chunk type.
438
+ * @param {Uint8Array} data Chunk data.
439
+ * @returns {Uint8Array}
440
+ */
441
+ static #pngChunk(type, data) {
442
+ const typeBytes = new TextEncoder().encode(type)
443
+ const chunk = new Uint8Array(12 + data.length)
444
+ const view = new DataView(chunk.buffer)
445
+
446
+ view.setUint32(0, data.length, false)
447
+ chunk.set(typeBytes, 4)
448
+ chunk.set(data, 8)
449
+ view.setUint32(
450
+ 8 + data.length,
451
+ SchematicImageParser.#crc32(
452
+ SchematicImageParser.#concatByteArrays([typeBytes, data])
453
+ ),
454
+ false
455
+ )
456
+
457
+ return chunk
458
+ }
459
+
460
+ /**
461
+ * Encodes raw scanlines as a zlib stream using stored deflate blocks.
462
+ * @param {Uint8Array} bytes Raw scanline bytes.
463
+ * @returns {Uint8Array}
464
+ */
465
+ static #zlibStore(bytes) {
466
+ const blockCount = Math.max(1, Math.ceil(bytes.length / 65535))
467
+ const output = new Uint8Array(2 + blockCount * 5 + bytes.length + 4)
468
+ const view = new DataView(output.buffer)
469
+ let outputOffset = 0
470
+ let inputOffset = 0
471
+
472
+ output[outputOffset] = 0x78
473
+ output[outputOffset + 1] = 0x01
474
+ outputOffset += 2
475
+
476
+ for (let blockIndex = 0; blockIndex < blockCount; blockIndex += 1) {
477
+ const remaining = bytes.length - inputOffset
478
+ const blockLength = Math.min(remaining, 65535)
479
+ output[outputOffset] = blockIndex === blockCount - 1 ? 0x01 : 0x00
480
+ view.setUint16(outputOffset + 1, blockLength, true)
481
+ view.setUint16(outputOffset + 3, 0xffff ^ blockLength, true)
482
+ outputOffset += 5
483
+ output.set(
484
+ bytes.slice(inputOffset, inputOffset + blockLength),
485
+ outputOffset
486
+ )
487
+ outputOffset += blockLength
488
+ inputOffset += blockLength
489
+ }
490
+
491
+ view.setUint32(
492
+ outputOffset,
493
+ SchematicImageParser.#adler32(bytes),
494
+ false
495
+ )
496
+
497
+ return output
498
+ }
499
+
500
+ /**
501
+ * Computes an Adler-32 checksum.
502
+ * @param {Uint8Array} bytes Bytes to checksum.
503
+ * @returns {number}
504
+ */
505
+ static #adler32(bytes) {
506
+ let a = 1
507
+ let b = 0
508
+
509
+ for (const byte of bytes) {
510
+ a = (a + byte) % 65521
511
+ b = (b + a) % 65521
512
+ }
513
+
514
+ return ((b << 16) | a) >>> 0
515
+ }
516
+
517
+ /**
518
+ * Computes a PNG-compatible CRC-32 checksum.
519
+ * @param {Uint8Array} bytes Bytes to checksum.
520
+ * @returns {number}
521
+ */
522
+ static #crc32(bytes) {
523
+ let crc = 0xffffffff
524
+
525
+ for (const byte of bytes) {
526
+ crc ^= byte
527
+ for (let bit = 0; bit < 8; bit += 1) {
528
+ crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0)
529
+ }
530
+ }
531
+
532
+ return (crc ^ 0xffffffff) >>> 0
533
+ }
534
+
535
+ /**
536
+ * Concatenates byte arrays.
537
+ * @param {Uint8Array[]} chunks Byte arrays.
538
+ * @returns {Uint8Array}
539
+ */
540
+ static #concatByteArrays(chunks) {
541
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0)
542
+ const output = new Uint8Array(totalLength)
543
+ let offset = 0
544
+
545
+ for (const chunk of chunks) {
546
+ output.set(chunk, offset)
547
+ offset += chunk.length
548
+ }
549
+
550
+ return output
131
551
  }
132
552
 
133
553
  /**
@@ -152,6 +572,57 @@ export class SchematicImageParser {
152
572
  return ''
153
573
  }
154
574
 
575
+ /**
576
+ * Detects common image MIME types from byte signatures.
577
+ * @param {Uint8Array} bytes Image bytes.
578
+ * @returns {string}
579
+ */
580
+ static #detectMimeType(bytes) {
581
+ if (!bytes || bytes.length < 4) {
582
+ return ''
583
+ }
584
+
585
+ if (PNG_SIGNATURE.every((byte, index) => bytes[index] === byte)) {
586
+ return 'image/png'
587
+ }
588
+ if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
589
+ return 'image/jpeg'
590
+ }
591
+ if (
592
+ bytes[0] === 0x47 &&
593
+ bytes[1] === 0x49 &&
594
+ bytes[2] === 0x46 &&
595
+ bytes[3] === 0x38
596
+ ) {
597
+ return 'image/gif'
598
+ }
599
+ if (bytes[0] === 0x42 && bytes[1] === 0x4d) {
600
+ return 'image/bmp'
601
+ }
602
+ if (
603
+ bytes.length >= 12 &&
604
+ bytes[0] === 0x52 &&
605
+ bytes[1] === 0x49 &&
606
+ bytes[2] === 0x46 &&
607
+ bytes[3] === 0x46 &&
608
+ bytes[8] === 0x57 &&
609
+ bytes[9] === 0x45 &&
610
+ bytes[10] === 0x42 &&
611
+ bytes[11] === 0x50
612
+ ) {
613
+ return 'image/webp'
614
+ }
615
+
616
+ const textPrefix = new TextDecoder('utf-8', { fatal: false })
617
+ .decode(bytes.slice(0, Math.min(bytes.length, 128)))
618
+ .trimStart()
619
+ if (textPrefix.startsWith('<svg') || textPrefix.startsWith('<?xml')) {
620
+ return 'image/svg+xml'
621
+ }
622
+
623
+ return ''
624
+ }
625
+
155
626
  /**
156
627
  * Encodes one byte array as base64 in both browser and test environments.
157
628
  * @param {Uint8Array} bytes