altium-toolkit 1.0.8 → 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 (88) 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/styles/altium-renderers.css +19 -0
  77. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  78. package/src/ui/PcbInteractionIndex.mjs +9 -4
  79. package/src/ui/PcbScene3dBuilder.mjs +137 -3
  80. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  81. package/src/ui/PcbSvgRenderer.mjs +1187 -34
  82. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  83. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  84. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  85. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  86. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  87. package/src/ui/SchematicTypography.mjs +48 -5
  88. package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
@@ -0,0 +1,240 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
6
+
7
+ /**
8
+ * Normalizes integrated-library extraction into the public parser model.
9
+ */
10
+ export class IntLibModelParser {
11
+ /**
12
+ * Builds one integrated-library model.
13
+ * @param {string} fileName
14
+ * @param {{ version?: string, crossReferences?: object[], parameters?: Record<string, string>, parameterRecords?: object[], sources?: object[], streamNames?: string[], diagnostics?: Record<string, number> } | null} extraction
15
+ * @returns {{ schema: string, kind: 'integrated-library', fileType: 'IntLib', fileName: string, summary: Record<string, number | string>, diagnostics: { severity: 'info' | 'warning', message: string }[], integratedLibrary: object, bom: [] }}
16
+ */
17
+ static parse(fileName, extraction = null) {
18
+ const normalizedExtraction = extraction || {}
19
+ const sources = Array.isArray(normalizedExtraction.sources)
20
+ ? normalizedExtraction.sources
21
+ : []
22
+ const crossReferences = Array.isArray(
23
+ normalizedExtraction.crossReferences
24
+ )
25
+ ? normalizedExtraction.crossReferences
26
+ : []
27
+ const parameters = normalizedExtraction.parameters || {}
28
+ const streamNames = Array.isArray(normalizedExtraction.streamNames)
29
+ ? normalizedExtraction.streamNames
30
+ : []
31
+
32
+ return NormalizedModelSchema.attach({
33
+ kind: 'integrated-library',
34
+ fileType: 'IntLib',
35
+ fileName,
36
+ summary: {
37
+ title: IntLibModelParser.#stripExtension(fileName),
38
+ version: normalizedExtraction.version || '',
39
+ sourceCount: sources.length,
40
+ crossReferenceCount: crossReferences.length,
41
+ parameterCount: Object.keys(parameters).length
42
+ },
43
+ diagnostics: IntLibModelParser.#buildDiagnostics(
44
+ streamNames,
45
+ sources,
46
+ crossReferences,
47
+ parameters,
48
+ normalizedExtraction.diagnostics?.issues || []
49
+ ),
50
+ integratedLibrary: {
51
+ version: normalizedExtraction.version || '',
52
+ streamNames,
53
+ crossReferences,
54
+ parameters,
55
+ parameterRecords: normalizedExtraction.parameterRecords || [],
56
+ diagnostics: {
57
+ ...(normalizedExtraction.diagnostics || {}),
58
+ issues: normalizedExtraction.diagnostics?.issues || []
59
+ },
60
+ indexes: IntLibModelParser.#buildIndexes(
61
+ sources,
62
+ crossReferences
63
+ ),
64
+ sources
65
+ },
66
+ bom: []
67
+ })
68
+ }
69
+
70
+ /**
71
+ * Builds source and model lookup indexes for bundled library consumers.
72
+ * @param {object[]} sources Bundled source entries.
73
+ * @param {object[]} crossReferences Cross-reference rows.
74
+ * @returns {object}
75
+ */
76
+ static #buildIndexes(sources, crossReferences) {
77
+ return {
78
+ sourcesByFileName:
79
+ IntLibModelParser.#buildSourcesByFileName(sources),
80
+ sourcesByKind: IntLibModelParser.#buildSourcesByKind(sources),
81
+ modelsByComponent:
82
+ IntLibModelParser.#buildModelsByComponent(crossReferences),
83
+ symbolsByComponent: IntLibModelParser.#buildModelNamesByKind(
84
+ crossReferences,
85
+ 'SCH'
86
+ ),
87
+ footprintsByComponent: IntLibModelParser.#buildModelNamesByKind(
88
+ crossReferences,
89
+ 'PCB'
90
+ )
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Indexes source entries by file name.
96
+ * @param {object[]} sources Source entries.
97
+ * @returns {Record<string, object>}
98
+ */
99
+ static #buildSourcesByFileName(sources) {
100
+ const index = {}
101
+ for (const [sourceIndex, source] of [...sources]
102
+ .map((source, index) => ({ ...source, sourceIndex: index }))
103
+ .sort((left, right) =>
104
+ String(left.fileName || '').localeCompare(
105
+ String(right.fileName || '')
106
+ )
107
+ )
108
+ .entries()) {
109
+ index[source.fileName] = {
110
+ index: source.sourceIndex ?? sourceIndex,
111
+ path: source.path,
112
+ fileType: source.fileType,
113
+ libraryKind: source.libraryKind
114
+ }
115
+ }
116
+ return index
117
+ }
118
+
119
+ /**
120
+ * Indexes source file names by source kind.
121
+ * @param {object[]} sources Source entries.
122
+ * @returns {Record<string, string[]>}
123
+ */
124
+ static #buildSourcesByKind(sources) {
125
+ const index = {}
126
+ for (const source of [...sources].sort((left, right) =>
127
+ String(left.libraryKind || '').localeCompare(
128
+ String(right.libraryKind || '')
129
+ )
130
+ )) {
131
+ const kind = source.libraryKind || 'other'
132
+ index[kind] ||= []
133
+ index[kind].push(source.fileName)
134
+ }
135
+ return index
136
+ }
137
+
138
+ /**
139
+ * Indexes cross-reference model rows by component name.
140
+ * @param {object[]} crossReferences Cross-reference rows.
141
+ * @returns {Record<string, object[]>}
142
+ */
143
+ static #buildModelsByComponent(crossReferences) {
144
+ const index = {}
145
+ for (const row of crossReferences || []) {
146
+ if (!row.component || !row.model) continue
147
+ index[row.component] ||= []
148
+ index[row.component].push({
149
+ model: row.model,
150
+ kind: row.kind || ''
151
+ })
152
+ }
153
+ return index
154
+ }
155
+
156
+ /**
157
+ * Indexes cross-reference model names of one kind by component.
158
+ * @param {object[]} crossReferences Cross-reference rows.
159
+ * @param {string} kind Model kind.
160
+ * @returns {Record<string, string[]>}
161
+ */
162
+ static #buildModelNamesByKind(crossReferences, kind) {
163
+ const index = {}
164
+ const lookup = kind.toUpperCase()
165
+ for (const row of crossReferences || []) {
166
+ if (
167
+ !row.component ||
168
+ !row.model ||
169
+ String(row.kind || '').toUpperCase() !== lookup
170
+ ) {
171
+ continue
172
+ }
173
+ index[row.component] ||= []
174
+ index[row.component].push(row.model)
175
+ }
176
+ return index
177
+ }
178
+
179
+ /**
180
+ * Builds parser diagnostics for one integrated-library model.
181
+ * @param {string[]} streamNames
182
+ * @param {object[]} sources
183
+ * @param {object[]} crossReferences
184
+ * @param {Record<string, string>} parameters
185
+ * @param {object[]} issues Structured parser issues.
186
+ * @returns {{ severity: 'info' | 'warning', message: string }[]}
187
+ */
188
+ static #buildDiagnostics(
189
+ streamNames,
190
+ sources,
191
+ crossReferences,
192
+ parameters,
193
+ issues
194
+ ) {
195
+ return [
196
+ {
197
+ severity: 'info',
198
+ message:
199
+ 'Recovered ' +
200
+ streamNames.length +
201
+ ' integrated-library data streams.'
202
+ },
203
+ {
204
+ severity: 'info',
205
+ message:
206
+ 'Recovered ' +
207
+ sources.length +
208
+ ' bundled library source entries.'
209
+ },
210
+ {
211
+ severity: 'info',
212
+ message:
213
+ 'Recovered ' +
214
+ crossReferences.length +
215
+ ' integrated-library cross references.'
216
+ },
217
+ {
218
+ severity: 'info',
219
+ message:
220
+ 'Recovered ' +
221
+ Object.keys(parameters).length +
222
+ ' integrated-library parameters.'
223
+ },
224
+ ...(issues || []).map((issue) => ({
225
+ severity: issue.severity || 'warning',
226
+ code: issue.code,
227
+ message: issue.message
228
+ }))
229
+ ]
230
+ }
231
+
232
+ /**
233
+ * Returns a file name without its last extension.
234
+ * @param {string} fileName
235
+ * @returns {string}
236
+ */
237
+ static #stripExtension(fileName) {
238
+ return String(fileName || '').replace(/\.[^.]+$/u, '')
239
+ }
240
+ }
@@ -0,0 +1,366 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { unzlibSync } from 'fflate'
6
+ import { OleCompoundDocument } from '../ole/OleCompoundDocument.mjs'
7
+ import { PcbSidecarRecordParser } from './PcbSidecarRecordParser.mjs'
8
+ import { PrintableTextDecoder } from './PrintableTextDecoder.mjs'
9
+
10
+ /**
11
+ * Extracts read-only metadata and bundled source entries from integrated
12
+ * library compound documents.
13
+ */
14
+ export class IntLibStreamExtractor {
15
+ /**
16
+ * Extracts integrated-library data directly from one OLE buffer.
17
+ * @param {ArrayBuffer} arrayBuffer
18
+ * @returns {ReturnType<typeof IntLibStreamExtractor.extractFromStreams> | null}
19
+ */
20
+ static extractFromArrayBuffer(arrayBuffer) {
21
+ const compoundDocument =
22
+ OleCompoundDocument.fromArrayBuffer(arrayBuffer)
23
+ const streams = new Map()
24
+
25
+ for (const name of compoundDocument.listStreams()) {
26
+ streams.set(name, compoundDocument.getStream(name))
27
+ }
28
+
29
+ return IntLibStreamExtractor.extractFromStreams(streams)
30
+ }
31
+
32
+ /**
33
+ * Extracts integrated-library metadata and source payloads from streams.
34
+ * @param {Map<string, Uint8Array>} streams
35
+ * @returns {{ version: string, crossReferences: object[], parameters: Record<string, string>, parameterRecords: object[], sources: object[], streamNames: string[], diagnostics: Record<string, number> }}
36
+ */
37
+ static extractFromStreams(streams) {
38
+ const version = IntLibStreamExtractor.#decodeText(
39
+ streams.get('Version.Txt')
40
+ ).trim()
41
+ const crossReferenceParse = IntLibStreamExtractor.#parseCrossReferences(
42
+ streams.get('LibCrossRef.Txt')
43
+ )
44
+ const crossReferences = crossReferenceParse.records
45
+ const parameterRecords = IntLibStreamExtractor.#parseParameterRecords(
46
+ streams.get('Parameters .bin')
47
+ )
48
+ const parameters =
49
+ IntLibStreamExtractor.#buildParameterMap(parameterRecords)
50
+ const sources = IntLibStreamExtractor.#extractSources(streams)
51
+ const streamNames = IntLibStreamExtractor.#collectUsedStreamNames(
52
+ streams,
53
+ sources
54
+ )
55
+
56
+ return {
57
+ version,
58
+ crossReferences,
59
+ parameters,
60
+ parameterRecords,
61
+ sources,
62
+ streamNames,
63
+ diagnostics: {
64
+ crossReferenceCount: crossReferences.length,
65
+ parameterCount: Object.keys(parameters).length,
66
+ sourceCount: sources.length,
67
+ issues: crossReferenceParse.issues
68
+ }
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Parses LibCrossRef.Txt into field records.
74
+ * @param {Uint8Array | undefined} bytes
75
+ * @returns {{ records: object[], issues: object[] }}
76
+ */
77
+ static #parseCrossReferences(bytes) {
78
+ const records = []
79
+ const issues = []
80
+
81
+ IntLibStreamExtractor.#decodeText(bytes)
82
+ .split(/\r?\n/u)
83
+ .map((line) => line.trim())
84
+ .filter(Boolean)
85
+ .forEach((line, index) => {
86
+ const fields = PcbSidecarRecordParser.parseRecordFields(
87
+ new TextEncoder().encode(line)
88
+ )
89
+ const component = fields.COMPONENT || fields.COMPONENTNAME || ''
90
+ const model = fields.MODEL || fields.MODELNAME || ''
91
+
92
+ if (!Object.keys(fields).length || !component || !model) {
93
+ issues.push(
94
+ IntLibStreamExtractor.#issue(
95
+ 'intlib.crossref.malformed-row',
96
+ 'LibCrossRef.Txt',
97
+ index + 1,
98
+ 'Skipped malformed integrated-library cross-reference row.'
99
+ )
100
+ )
101
+ return
102
+ }
103
+
104
+ records.push({
105
+ component,
106
+ model,
107
+ kind: fields.KIND || fields.TYPE || '',
108
+ fields
109
+ })
110
+ })
111
+
112
+ return { records, issues }
113
+ }
114
+
115
+ /**
116
+ * Builds a structured parser issue.
117
+ * @param {string} code Stable diagnostic code.
118
+ * @param {string} stream Source stream name.
119
+ * @param {number} line One-based line number.
120
+ * @param {string} message User-facing summary.
121
+ * @returns {object}
122
+ */
123
+ static #issue(code, stream, line, message) {
124
+ return {
125
+ code,
126
+ severity: 'warning',
127
+ stream,
128
+ line,
129
+ message
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Parses integrated-library parameter records.
135
+ * @param {Uint8Array | undefined} bytes
136
+ * @returns {object[]}
137
+ */
138
+ static #parseParameterRecords(bytes) {
139
+ const lengthPrefixed =
140
+ PcbSidecarRecordParser.parseLengthPrefixedRecords(
141
+ bytes,
142
+ 'Parameters .bin'
143
+ )
144
+
145
+ if (lengthPrefixed.length) {
146
+ return lengthPrefixed.map((record) => record.fields)
147
+ }
148
+
149
+ return IntLibStreamExtractor.#decodeText(bytes)
150
+ .split(/\r?\n/u)
151
+ .map((line) => line.trim())
152
+ .filter(Boolean)
153
+ .map((line) =>
154
+ PcbSidecarRecordParser.parseRecordFields(
155
+ new TextEncoder().encode(line)
156
+ )
157
+ )
158
+ .filter((fields) => Object.keys(fields).length)
159
+ }
160
+
161
+ /**
162
+ * Builds a parameter lookup from parameter records.
163
+ * @param {Record<string, string>[]} records
164
+ * @returns {Record<string, string>}
165
+ */
166
+ static #buildParameterMap(records) {
167
+ const parameters = {}
168
+
169
+ for (const record of records) {
170
+ const name = record.NAME || record.PARAMETER || ''
171
+ if (!name) {
172
+ continue
173
+ }
174
+
175
+ parameters[name] = record.VALUE || ''
176
+ }
177
+
178
+ return parameters
179
+ }
180
+
181
+ /**
182
+ * Extracts bundled source entries from recognized source-library folders.
183
+ * @param {Map<string, Uint8Array>} streams
184
+ * @returns {object[]}
185
+ */
186
+ static #extractSources(streams) {
187
+ const sources = []
188
+
189
+ for (const [path, bytes] of streams.entries()) {
190
+ const sourceKind = IntLibStreamExtractor.#sourceKind(path)
191
+ if (!sourceKind) {
192
+ continue
193
+ }
194
+
195
+ const payload = IntLibStreamExtractor.#decodePayload(bytes)
196
+ sources.push({
197
+ path,
198
+ fileName: IntLibStreamExtractor.#basename(path),
199
+ fileType: IntLibStreamExtractor.#sourceFileType(path),
200
+ libraryKind: sourceKind,
201
+ compression: payload.compression,
202
+ byteLength: payload.bytes.byteLength,
203
+ payloadBase64: IntLibStreamExtractor.#bytesToBase64(
204
+ payload.bytes
205
+ ),
206
+ payloadText: IntLibStreamExtractor.#decodePrintablePayload(
207
+ payload.bytes
208
+ )
209
+ })
210
+ }
211
+
212
+ return sources.sort((left, right) =>
213
+ left.path.localeCompare(right.path)
214
+ )
215
+ }
216
+
217
+ /**
218
+ * Decodes a possibly wrapped zlib source payload.
219
+ * @param {Uint8Array} bytes
220
+ * @returns {{ bytes: Uint8Array, compression: string }}
221
+ */
222
+ static #decodePayload(bytes) {
223
+ const normalized = PcbSidecarRecordParser.toUint8Array(bytes)
224
+
225
+ if (normalized[0] === 0x02 && normalized[1] === 0x78) {
226
+ return {
227
+ bytes: Uint8Array.from(unzlibSync(normalized.subarray(1))),
228
+ compression: 'zlib-wrapper'
229
+ }
230
+ }
231
+
232
+ if (normalized[0] === 0x78) {
233
+ return {
234
+ bytes: Uint8Array.from(unzlibSync(normalized)),
235
+ compression: 'zlib'
236
+ }
237
+ }
238
+
239
+ return {
240
+ bytes: normalized,
241
+ compression: 'none'
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Decodes text bytes with toolkit printable-text fallbacks.
247
+ * @param {Uint8Array | undefined} bytes
248
+ * @returns {string}
249
+ */
250
+ static #decodeText(bytes) {
251
+ if (!bytes) {
252
+ return ''
253
+ }
254
+
255
+ return PrintableTextDecoder.decodeBytes(
256
+ PcbSidecarRecordParser.toUint8Array(bytes)
257
+ ).replace(/^\uFEFF/u, '')
258
+ }
259
+
260
+ /**
261
+ * Decodes source payload text only when it looks printable.
262
+ * @param {Uint8Array} bytes
263
+ * @returns {string}
264
+ */
265
+ static #decodePrintablePayload(bytes) {
266
+ const text = IntLibStreamExtractor.#decodeText(bytes)
267
+ .replace(/\u0000/gu, '')
268
+ .trim()
269
+ if (!text || /[\u0001-\u0008\u000b\u000c\u000e-\u001f]/u.test(text)) {
270
+ return ''
271
+ }
272
+
273
+ return text
274
+ }
275
+
276
+ /**
277
+ * Resolves the source-library kind for one stream path.
278
+ * @param {string} path
279
+ * @returns {string}
280
+ */
281
+ static #sourceKind(path) {
282
+ const normalized = String(path || '').replace(/\\/gu, '/')
283
+
284
+ if (/^SchLib\//iu.test(normalized)) {
285
+ return 'schematic-symbols'
286
+ }
287
+ if (/^PCBLib\//iu.test(normalized)) {
288
+ return 'pcb-footprints'
289
+ }
290
+ if (/^PCB3DLib\//iu.test(normalized)) {
291
+ return 'pcb-3d-models'
292
+ }
293
+
294
+ return ''
295
+ }
296
+
297
+ /**
298
+ * Resolves the source file type from one stream path.
299
+ * @param {string} path
300
+ * @returns {string}
301
+ */
302
+ static #sourceFileType(path) {
303
+ const extension = IntLibStreamExtractor.#basename(path).split('.').pop()
304
+
305
+ if (/^SchLib$/iu.test(extension)) return 'SchLib'
306
+ if (/^PcbLib$/iu.test(extension)) return 'PcbLib'
307
+ if (/^PCB3DLib$/iu.test(extension)) return 'PCB3DLib'
308
+
309
+ return extension || 'unknown'
310
+ }
311
+
312
+ /**
313
+ * Returns the final path segment.
314
+ * @param {string} path
315
+ * @returns {string}
316
+ */
317
+ static #basename(path) {
318
+ return String(path || '')
319
+ .replace(/\\/gu, '/')
320
+ .split('/')
321
+ .pop()
322
+ }
323
+
324
+ /**
325
+ * Collects streams that contributed to extraction.
326
+ * @param {Map<string, Uint8Array>} streams
327
+ * @param {object[]} sources
328
+ * @returns {string[]}
329
+ */
330
+ static #collectUsedStreamNames(streams, sources) {
331
+ return [
332
+ 'Version.Txt',
333
+ 'LibCrossRef.Txt',
334
+ 'Parameters .bin',
335
+ ...sources.map((source) => source.path)
336
+ ]
337
+ .filter((streamName) => streams.has(streamName))
338
+ .sort((left, right) => left.localeCompare(right))
339
+ }
340
+
341
+ /**
342
+ * Encodes bytes as base64 in browser and Node runtimes.
343
+ * @param {Uint8Array} bytes
344
+ * @returns {string}
345
+ */
346
+ static #bytesToBase64(bytes) {
347
+ if (typeof btoa === 'function') {
348
+ let binary = ''
349
+ const chunkSize = 0x8000
350
+
351
+ for (
352
+ let offset = 0;
353
+ offset < bytes.byteLength;
354
+ offset += chunkSize
355
+ ) {
356
+ binary += String.fromCharCode(
357
+ ...bytes.subarray(offset, offset + chunkSize)
358
+ )
359
+ }
360
+
361
+ return btoa(binary)
362
+ }
363
+
364
+ return Buffer.from(bytes).toString('base64')
365
+ }
366
+ }