altium-toolkit 0.1.1 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -6
- package/docs/api.md +42 -4
- package/docs/model-format.md +95 -5
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +553 -0
- package/docs/testing.md +7 -2
- package/package.json +6 -2
- package/spec/library-scope.md +7 -1
- package/src/core/altium/AltiumParser.mjs +22 -325
- package/src/core/altium/NormalizedModelSchema.mjs +28 -0
- package/src/core/altium/PcbArcPrimitiveParser.mjs +87 -0
- package/src/core/altium/PcbBinaryPrimitiveParser.mjs +43 -370
- package/src/core/altium/PcbBoardRegionSemanticsParser.mjs +477 -0
- package/src/core/altium/PcbComponentAnnotationNormalizer.mjs +290 -0
- package/src/core/altium/PcbComponentBodyPlacementNormalizer.mjs +52 -0
- package/src/core/altium/PcbComponentPrimitiveIndexer.mjs +109 -0
- package/src/core/altium/PcbEmbeddedFontExtractor.mjs +484 -0
- package/src/core/altium/PcbFillPrimitiveParser.mjs +84 -0
- package/src/core/altium/PcbFontMetricsParser.mjs +308 -0
- package/src/core/altium/PcbGeometryFlipper.mjs +244 -0
- package/src/core/altium/PcbLayerIdCodec.mjs +136 -0
- package/src/core/altium/PcbLibModelParser.mjs +202 -0
- package/src/core/altium/PcbLibStreamExtractor.mjs +968 -0
- package/src/core/altium/PcbModelParser.mjs +618 -66
- package/src/core/altium/PcbOutlineRecovery.mjs +4 -112
- package/src/core/altium/PcbPadPrimitiveParser.mjs +347 -0
- package/src/core/altium/PcbPadShapeCodec.mjs +158 -0
- package/src/core/altium/PcbPadStackParser.mjs +903 -0
- package/src/core/altium/PcbPrimitiveOwnershipIndexParser.mjs +60 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +212 -0
- package/src/core/altium/PcbPrimitiveRecordSlicer.mjs +243 -0
- package/src/core/altium/PcbRawRecordRegistry.mjs +831 -0
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +317 -0
- package/src/core/altium/PcbRuleParser.mjs +587 -0
- package/src/core/altium/PcbStreamExtractor.mjs +127 -4
- package/src/core/altium/PcbTextPrimitiveParser.mjs +537 -0
- package/src/core/altium/PcbTrackPrimitiveParser.mjs +87 -0
- package/src/core/altium/PcbViaPrimitiveParser.mjs +88 -0
- package/src/core/altium/PcbViaStackParser.mjs +548 -0
- package/src/core/altium/PcbWideStringTableParser.mjs +108 -0
- package/src/core/altium/PrjPcbModelParser.mjs +797 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +355 -0
- package/src/parser.mjs +13 -0
- package/src/renderers.mjs +5 -0
- package/src/styles/altium-renderers.css +11 -6
- package/src/ui/PcbCopperPrimitiveSplitter.mjs +113 -0
- package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +6 -5
- package/src/ui/PcbEmbeddedFontFaceRenderer.mjs +126 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +27 -6
- package/src/ui/PcbRegionPrimitiveRenderer.mjs +243 -0
- package/src/ui/PcbSideResolvedRenderModel.mjs +336 -0
- package/src/ui/PcbSvgRenderer.mjs +101 -109
- package/src/ui/PcbTextPrimitiveRenderer.mjs +252 -0
- package/src/ui/SchematicSheetChromeRenderer.mjs +2 -93
- package/src/ui/SchematicSheetZoneRenderer.mjs +104 -0
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { PcbBinaryPrimitiveParser } from './PcbBinaryPrimitiveParser.mjs'
|
|
6
|
+
import { PcbEmbeddedFontExtractor } from './PcbEmbeddedFontExtractor.mjs'
|
|
7
|
+
import { PcbRawRecordRegistry } from './PcbRawRecordRegistry.mjs'
|
|
8
|
+
import { OleCompoundDocument } from '../ole/OleCompoundDocument.mjs'
|
|
9
|
+
import { OleConstants } from '../ole/OleConstants.mjs'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extracts footprint-oriented content from OLE-backed Altium PcbLib files.
|
|
13
|
+
*/
|
|
14
|
+
export class PcbLibStreamExtractor {
|
|
15
|
+
static #FOOTPRINT_RECORD_TYPES = {
|
|
16
|
+
1: {
|
|
17
|
+
type: 'arc',
|
|
18
|
+
collection: 'arcs',
|
|
19
|
+
minimumSubrecordCount: 1,
|
|
20
|
+
minimumPayloadByteLength: 45,
|
|
21
|
+
parser: 'parseArcStream'
|
|
22
|
+
},
|
|
23
|
+
2: {
|
|
24
|
+
type: 'pad',
|
|
25
|
+
collection: 'pads',
|
|
26
|
+
minimumSubrecordCount: 6,
|
|
27
|
+
validatedSubrecordIndex: 4,
|
|
28
|
+
minimumPayloadByteLength: 61,
|
|
29
|
+
parser: 'parsePadStream'
|
|
30
|
+
},
|
|
31
|
+
3: {
|
|
32
|
+
type: 'via',
|
|
33
|
+
collection: 'vias',
|
|
34
|
+
minimumSubrecordCount: 1,
|
|
35
|
+
minimumPayloadByteLength: 321,
|
|
36
|
+
parser: 'parseViaStream'
|
|
37
|
+
},
|
|
38
|
+
4: {
|
|
39
|
+
type: 'track',
|
|
40
|
+
collection: 'tracks',
|
|
41
|
+
minimumSubrecordCount: 1,
|
|
42
|
+
minimumPayloadByteLength: 33,
|
|
43
|
+
parser: 'parseTrackStream'
|
|
44
|
+
},
|
|
45
|
+
5: {
|
|
46
|
+
type: 'text',
|
|
47
|
+
collection: 'texts',
|
|
48
|
+
minimumSubrecordCount: 2,
|
|
49
|
+
validatedSubrecordIndex: 0,
|
|
50
|
+
minimumPayloadByteLength: 64,
|
|
51
|
+
parser: 'parseTextStream'
|
|
52
|
+
},
|
|
53
|
+
6: {
|
|
54
|
+
type: 'fill',
|
|
55
|
+
collection: 'fills',
|
|
56
|
+
minimumSubrecordCount: 1,
|
|
57
|
+
minimumPayloadByteLength: 50,
|
|
58
|
+
parser: 'parseFillStream'
|
|
59
|
+
},
|
|
60
|
+
11: {
|
|
61
|
+
type: 'region',
|
|
62
|
+
collection: 'regions',
|
|
63
|
+
minimumSubrecordCount: 1,
|
|
64
|
+
minimumPayloadByteLength: 18,
|
|
65
|
+
parser: 'parseRegionStream'
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Returns true when one buffer starts with the OLE compound-document
|
|
71
|
+
* signature.
|
|
72
|
+
* @param {ArrayBuffer} arrayBuffer
|
|
73
|
+
* @returns {boolean}
|
|
74
|
+
*/
|
|
75
|
+
static isCompoundDocument(arrayBuffer) {
|
|
76
|
+
const bytes = new Uint8Array(
|
|
77
|
+
arrayBuffer,
|
|
78
|
+
0,
|
|
79
|
+
Math.min(
|
|
80
|
+
arrayBuffer.byteLength,
|
|
81
|
+
OleConstants.HEADER_SIGNATURE.length
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if (bytes.byteLength < OleConstants.HEADER_SIGNATURE.length) {
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return OleConstants.HEADER_SIGNATURE.every(
|
|
90
|
+
(value, index) => bytes[index] === value
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extracts PcbLib content directly from one OLE-backed buffer.
|
|
96
|
+
* @param {ArrayBuffer} arrayBuffer
|
|
97
|
+
* @returns {{ libraryHeader: Record<string, string>, componentParamsToc: Record<string, object>, sectionKeys: Record<string, string>, footprints: object[], streamNames: string[], diagnostics: Record<string, number> } | null}
|
|
98
|
+
*/
|
|
99
|
+
static extractFromArrayBuffer(arrayBuffer) {
|
|
100
|
+
if (!PcbLibStreamExtractor.isCompoundDocument(arrayBuffer)) {
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const compoundDocument =
|
|
105
|
+
OleCompoundDocument.fromArrayBuffer(arrayBuffer)
|
|
106
|
+
const streams = new Map()
|
|
107
|
+
|
|
108
|
+
for (const name of compoundDocument.listStreams()) {
|
|
109
|
+
streams.set(name, compoundDocument.getStream(name))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return PcbLibStreamExtractor.extractFromStreams(streams)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Extracts all declared footprints from one PcbLib stream map.
|
|
117
|
+
* @param {Map<string, Uint8Array>} streams
|
|
118
|
+
* @returns {{ libraryHeader: Record<string, string>, componentParamsToc: Record<string, object>, sectionKeys: Record<string, string>, footprints: object[], streamNames: string[], diagnostics: Record<string, number> }}
|
|
119
|
+
*/
|
|
120
|
+
static extractFromStreams(streams) {
|
|
121
|
+
const libraryData = streams.get('Library/Data') || new Uint8Array()
|
|
122
|
+
const parsedLibraryData =
|
|
123
|
+
PcbLibStreamExtractor.#parseLibraryData(libraryData)
|
|
124
|
+
const componentParamsToc =
|
|
125
|
+
PcbLibStreamExtractor.#parseComponentParamsToc(
|
|
126
|
+
streams.get('Library/ComponentParamsTOC/Data')
|
|
127
|
+
)
|
|
128
|
+
const sectionKeys = PcbLibStreamExtractor.#parseSectionKeys(
|
|
129
|
+
streams.get('SectionKeys')
|
|
130
|
+
)
|
|
131
|
+
const footprints = parsedLibraryData.footprintNames.flatMap((name) => {
|
|
132
|
+
const resolvedStorage =
|
|
133
|
+
PcbLibStreamExtractor.#resolveFootprintStorageName(
|
|
134
|
+
streams,
|
|
135
|
+
name,
|
|
136
|
+
sectionKeys
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if (!resolvedStorage) {
|
|
140
|
+
return []
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return [
|
|
144
|
+
PcbLibStreamExtractor.#extractFootprint(
|
|
145
|
+
streams,
|
|
146
|
+
name,
|
|
147
|
+
resolvedStorage,
|
|
148
|
+
componentParamsToc[name] || {}
|
|
149
|
+
)
|
|
150
|
+
]
|
|
151
|
+
})
|
|
152
|
+
const primitiveCount = footprints.reduce(
|
|
153
|
+
(sum, footprint) => sum + footprint.primitiveCount,
|
|
154
|
+
0
|
|
155
|
+
)
|
|
156
|
+
const rawRecordCount = footprints.reduce(
|
|
157
|
+
(sum, footprint) => sum + footprint.rawRecords.length,
|
|
158
|
+
0
|
|
159
|
+
)
|
|
160
|
+
const embeddedFonts =
|
|
161
|
+
PcbEmbeddedFontExtractor.extractFromStreams(streams)
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
libraryHeader: parsedLibraryData.libraryHeader,
|
|
165
|
+
componentParamsToc,
|
|
166
|
+
sectionKeys,
|
|
167
|
+
footprints,
|
|
168
|
+
streamNames: PcbLibStreamExtractor.#collectUsedStreamNames(
|
|
169
|
+
footprints,
|
|
170
|
+
streams,
|
|
171
|
+
embeddedFonts
|
|
172
|
+
),
|
|
173
|
+
embeddedFonts,
|
|
174
|
+
diagnostics: {
|
|
175
|
+
declaredFootprintCount: parsedLibraryData.footprintNames.length,
|
|
176
|
+
footprintCount: footprints.length,
|
|
177
|
+
primitiveCount,
|
|
178
|
+
rawRecordCount,
|
|
179
|
+
embeddedFontCount: embeddedFonts.fonts.length,
|
|
180
|
+
missingFootprintCount:
|
|
181
|
+
parsedLibraryData.footprintNames.length - footprints.length
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Parses one PcbLib Library/Data stream.
|
|
188
|
+
* @param {Uint8Array} bytes
|
|
189
|
+
* @returns {{ libraryHeader: Record<string, string>, footprintNames: string[] }}
|
|
190
|
+
*/
|
|
191
|
+
static #parseLibraryData(bytes) {
|
|
192
|
+
const header = PcbLibStreamExtractor.#readLengthPrefixedTextAt(bytes, 0)
|
|
193
|
+
const libraryHeader = header
|
|
194
|
+
? PcbLibStreamExtractor.#parsePipeProperties(header.text)
|
|
195
|
+
: {}
|
|
196
|
+
let offset = header ? header.nextOffset : 0
|
|
197
|
+
|
|
198
|
+
if (offset + 4 > bytes.byteLength) {
|
|
199
|
+
return { libraryHeader, footprintNames: [] }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const count = PcbLibStreamExtractor.#readUint32(bytes, offset)
|
|
203
|
+
offset += 4
|
|
204
|
+
|
|
205
|
+
const footprintNames = []
|
|
206
|
+
for (let index = 0; index < count; index += 1) {
|
|
207
|
+
const block = PcbLibStreamExtractor.#readStringBlockAt(
|
|
208
|
+
bytes,
|
|
209
|
+
offset
|
|
210
|
+
)
|
|
211
|
+
if (!block) {
|
|
212
|
+
break
|
|
213
|
+
}
|
|
214
|
+
footprintNames.push(block.text)
|
|
215
|
+
offset = block.nextOffset
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { libraryHeader, footprintNames }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Parses the optional ComponentParamsTOC stream into entries keyed by name.
|
|
223
|
+
* @param {Uint8Array | undefined} bytes
|
|
224
|
+
* @returns {Record<string, { name: string, padCount: number, height: string, description: string, properties: Record<string, string> }>}
|
|
225
|
+
*/
|
|
226
|
+
static #parseComponentParamsToc(bytes) {
|
|
227
|
+
const entries = {}
|
|
228
|
+
let offset = 0
|
|
229
|
+
|
|
230
|
+
while (bytes && offset + 4 <= bytes.byteLength) {
|
|
231
|
+
const record = PcbLibStreamExtractor.#readLengthPrefixedTextAt(
|
|
232
|
+
bytes,
|
|
233
|
+
offset
|
|
234
|
+
)
|
|
235
|
+
if (!record) {
|
|
236
|
+
break
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const properties = PcbLibStreamExtractor.#parsePipeProperties(
|
|
240
|
+
record.text
|
|
241
|
+
)
|
|
242
|
+
const name = properties.Name || properties.NAME || ''
|
|
243
|
+
if (name) {
|
|
244
|
+
entries[name] = {
|
|
245
|
+
name,
|
|
246
|
+
padCount: Number(
|
|
247
|
+
properties['Pad Count'] || properties.PADCOUNT || 0
|
|
248
|
+
),
|
|
249
|
+
height: properties.Height || properties.HEIGHT || '',
|
|
250
|
+
description:
|
|
251
|
+
properties.Description || properties.DESCRIPTION || '',
|
|
252
|
+
properties
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
offset = record.nextOffset
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return entries
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Parses an optional SectionKeys stream that maps full footprint names to
|
|
264
|
+
* shortened OLE storage names.
|
|
265
|
+
* @param {Uint8Array | undefined} bytes
|
|
266
|
+
* @returns {Record<string, string>}
|
|
267
|
+
*/
|
|
268
|
+
static #parseSectionKeys(bytes) {
|
|
269
|
+
if (!bytes || bytes.byteLength < 4) {
|
|
270
|
+
return {}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const count = PcbLibStreamExtractor.#readUint32(bytes, 0)
|
|
274
|
+
const entries = {}
|
|
275
|
+
let offset = 4
|
|
276
|
+
|
|
277
|
+
for (let index = 0; index < count; index += 1) {
|
|
278
|
+
const fullName = PcbLibStreamExtractor.#readStringBlockAt(
|
|
279
|
+
bytes,
|
|
280
|
+
offset
|
|
281
|
+
)
|
|
282
|
+
if (!fullName) {
|
|
283
|
+
break
|
|
284
|
+
}
|
|
285
|
+
offset = fullName.nextOffset
|
|
286
|
+
|
|
287
|
+
const storageName = PcbLibStreamExtractor.#readStringBlockAt(
|
|
288
|
+
bytes,
|
|
289
|
+
offset
|
|
290
|
+
)
|
|
291
|
+
if (!storageName) {
|
|
292
|
+
break
|
|
293
|
+
}
|
|
294
|
+
offset = storageName.nextOffset
|
|
295
|
+
entries[fullName.text] = storageName.text
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return entries
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Resolves the OLE storage name for one declared footprint.
|
|
303
|
+
* @param {Map<string, Uint8Array>} streams
|
|
304
|
+
* @param {string} footprintName
|
|
305
|
+
* @param {Record<string, string>} sectionKeys
|
|
306
|
+
* @returns {string}
|
|
307
|
+
*/
|
|
308
|
+
static #resolveFootprintStorageName(streams, footprintName, sectionKeys) {
|
|
309
|
+
const candidates = [
|
|
310
|
+
footprintName,
|
|
311
|
+
PcbLibStreamExtractor.#sanitizeStorageName(footprintName),
|
|
312
|
+
sectionKeys[footprintName],
|
|
313
|
+
PcbLibStreamExtractor.#sanitizeStorageName(
|
|
314
|
+
footprintName.slice(0, 31)
|
|
315
|
+
)
|
|
316
|
+
].filter(Boolean)
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
candidates.find((candidate) => streams.has(candidate + '/Data')) ||
|
|
320
|
+
''
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Extracts one footprint storage into normalized primitive lists.
|
|
326
|
+
* @param {Map<string, Uint8Array>} streams
|
|
327
|
+
* @param {string} name
|
|
328
|
+
* @param {string} storageName
|
|
329
|
+
* @param {object} componentParams
|
|
330
|
+
* @returns {object}
|
|
331
|
+
*/
|
|
332
|
+
static #extractFootprint(streams, name, storageName, componentParams) {
|
|
333
|
+
const declaredPrimitiveCount = PcbLibStreamExtractor.#readCountHeader(
|
|
334
|
+
streams.get(storageName + '/Header')
|
|
335
|
+
)
|
|
336
|
+
const parameters = PcbLibStreamExtractor.#parsePropertyStream(
|
|
337
|
+
streams.get(storageName + '/Parameters')
|
|
338
|
+
)
|
|
339
|
+
const wideStrings = PcbLibStreamExtractor.#parseWideStrings(
|
|
340
|
+
streams.get(storageName + '/WideStrings')
|
|
341
|
+
)
|
|
342
|
+
const parsedData = PcbLibStreamExtractor.#parseFootprintData(
|
|
343
|
+
streams.get(storageName + '/Data') || new Uint8Array(),
|
|
344
|
+
declaredPrimitiveCount,
|
|
345
|
+
wideStrings,
|
|
346
|
+
storageName
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
name,
|
|
351
|
+
dataName: parsedData.dataName || name,
|
|
352
|
+
sourceStorage: storageName,
|
|
353
|
+
declaredPrimitiveCount,
|
|
354
|
+
parameters,
|
|
355
|
+
componentParams,
|
|
356
|
+
wideStrings,
|
|
357
|
+
primitiveCount: parsedData.primitiveOrder.length,
|
|
358
|
+
primitiveOrder: parsedData.primitiveOrder,
|
|
359
|
+
unknownRecords: parsedData.unknownRecords,
|
|
360
|
+
rawRecords: parsedData.rawRecords,
|
|
361
|
+
pads: parsedData.pads,
|
|
362
|
+
tracks: parsedData.tracks,
|
|
363
|
+
arcs: parsedData.arcs,
|
|
364
|
+
vias: parsedData.vias,
|
|
365
|
+
fills: parsedData.fills,
|
|
366
|
+
texts: parsedData.texts,
|
|
367
|
+
regions: parsedData.regions
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Parses one footprint Data stream after its leading name block.
|
|
373
|
+
* @param {Uint8Array} bytes
|
|
374
|
+
* @param {number} declaredPrimitiveCount
|
|
375
|
+
* @param {Record<number, string>} wideStrings
|
|
376
|
+
* @param {string} sourceStorage
|
|
377
|
+
* @returns {{ dataName: string, primitiveOrder: object[], unknownRecords: object[], rawRecords: object[], pads: object[], tracks: object[], arcs: object[], vias: object[], fills: object[], texts: object[], regions: object[] }}
|
|
378
|
+
*/
|
|
379
|
+
static #parseFootprintData(
|
|
380
|
+
bytes,
|
|
381
|
+
declaredPrimitiveCount,
|
|
382
|
+
wideStrings,
|
|
383
|
+
sourceStorage
|
|
384
|
+
) {
|
|
385
|
+
const collections = PcbLibStreamExtractor.#createPrimitiveCollections()
|
|
386
|
+
const dataName = PcbLibStreamExtractor.#readStringBlockAt(bytes, 0)
|
|
387
|
+
let offset = dataName ? dataName.nextOffset : 0
|
|
388
|
+
let parsedCount = 0
|
|
389
|
+
|
|
390
|
+
while (
|
|
391
|
+
offset < bytes.byteLength &&
|
|
392
|
+
(!declaredPrimitiveCount || parsedCount < declaredPrimitiveCount)
|
|
393
|
+
) {
|
|
394
|
+
if (bytes[offset] === 0) {
|
|
395
|
+
break
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const remainingCount = declaredPrimitiveCount
|
|
399
|
+
? declaredPrimitiveCount - parsedCount - 1
|
|
400
|
+
: null
|
|
401
|
+
const record = PcbLibStreamExtractor.#readFootprintRecordAt(
|
|
402
|
+
bytes,
|
|
403
|
+
offset,
|
|
404
|
+
remainingCount
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
if (!record) {
|
|
408
|
+
break
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
PcbLibStreamExtractor.#appendFootprintRecord(
|
|
412
|
+
collections,
|
|
413
|
+
record,
|
|
414
|
+
wideStrings,
|
|
415
|
+
sourceStorage,
|
|
416
|
+
parsedCount
|
|
417
|
+
)
|
|
418
|
+
offset += record.byteLength
|
|
419
|
+
parsedCount += 1
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
dataName: dataName?.text || '',
|
|
424
|
+
...collections
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Creates the mutable primitive collection object used while parsing one
|
|
430
|
+
* footprint.
|
|
431
|
+
* @returns {{ primitiveOrder: object[], unknownRecords: object[], rawRecords: object[], pads: object[], tracks: object[], arcs: object[], vias: object[], fills: object[], texts: object[], regions: object[] }}
|
|
432
|
+
*/
|
|
433
|
+
static #createPrimitiveCollections() {
|
|
434
|
+
return {
|
|
435
|
+
primitiveOrder: [],
|
|
436
|
+
unknownRecords: [],
|
|
437
|
+
rawRecords: [],
|
|
438
|
+
pads: [],
|
|
439
|
+
tracks: [],
|
|
440
|
+
arcs: [],
|
|
441
|
+
vias: [],
|
|
442
|
+
fills: [],
|
|
443
|
+
texts: [],
|
|
444
|
+
regions: []
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Reads one mixed-format PcbLib footprint record.
|
|
450
|
+
* @param {Uint8Array} bytes
|
|
451
|
+
* @param {number} offset
|
|
452
|
+
* @param {number | null} remainingCount
|
|
453
|
+
* @returns {{ typeId: number, descriptor: object | null, recordBytes: Uint8Array, byteLength: number, offset: number } | null}
|
|
454
|
+
*/
|
|
455
|
+
static #readFootprintRecordAt(bytes, offset, remainingCount) {
|
|
456
|
+
if (offset >= bytes.byteLength) {
|
|
457
|
+
return null
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const typeId = bytes[offset]
|
|
461
|
+
const descriptor =
|
|
462
|
+
PcbLibStreamExtractor.#FOOTPRINT_RECORD_TYPES[typeId] || null
|
|
463
|
+
|
|
464
|
+
if (!descriptor) {
|
|
465
|
+
return {
|
|
466
|
+
typeId,
|
|
467
|
+
descriptor: null,
|
|
468
|
+
recordBytes: bytes.slice(offset, offset + 1),
|
|
469
|
+
byteLength: 1,
|
|
470
|
+
offset
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const minimumEnd = PcbLibStreamExtractor.#readMinimumRecordEnd(
|
|
475
|
+
bytes,
|
|
476
|
+
offset,
|
|
477
|
+
descriptor
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
if (!minimumEnd) {
|
|
481
|
+
return {
|
|
482
|
+
typeId,
|
|
483
|
+
descriptor: null,
|
|
484
|
+
recordBytes: bytes.slice(offset, offset + 1),
|
|
485
|
+
byteLength: 1,
|
|
486
|
+
offset
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const nextRecordOffset =
|
|
491
|
+
remainingCount && remainingCount > 0
|
|
492
|
+
? PcbLibStreamExtractor.#findNextKnownRecordOffset(
|
|
493
|
+
bytes,
|
|
494
|
+
minimumEnd
|
|
495
|
+
)
|
|
496
|
+
: null
|
|
497
|
+
const endOffset = nextRecordOffset || minimumEnd
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
typeId,
|
|
501
|
+
descriptor,
|
|
502
|
+
recordBytes: bytes.slice(offset, endOffset),
|
|
503
|
+
byteLength: endOffset - offset,
|
|
504
|
+
offset
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Reads the minimum byte boundary for a known footprint record.
|
|
510
|
+
* @param {Uint8Array} bytes
|
|
511
|
+
* @param {number} offset
|
|
512
|
+
* @param {{ minimumSubrecordCount: number, minimumPayloadByteLength: number, validatedSubrecordIndex?: number }} descriptor
|
|
513
|
+
* @returns {number | null}
|
|
514
|
+
*/
|
|
515
|
+
static #readMinimumRecordEnd(bytes, offset, descriptor) {
|
|
516
|
+
let cursor = offset + 1
|
|
517
|
+
|
|
518
|
+
for (
|
|
519
|
+
let subrecordIndex = 0;
|
|
520
|
+
subrecordIndex < descriptor.minimumSubrecordCount;
|
|
521
|
+
subrecordIndex += 1
|
|
522
|
+
) {
|
|
523
|
+
const subrecord = PcbLibStreamExtractor.#readSubrecordAt(
|
|
524
|
+
bytes,
|
|
525
|
+
cursor
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
if (!subrecord) {
|
|
529
|
+
return null
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const shouldValidate =
|
|
533
|
+
descriptor.validatedSubrecordIndex === undefined ||
|
|
534
|
+
descriptor.validatedSubrecordIndex === subrecordIndex
|
|
535
|
+
if (
|
|
536
|
+
shouldValidate &&
|
|
537
|
+
subrecord.payloadByteLength <
|
|
538
|
+
descriptor.minimumPayloadByteLength
|
|
539
|
+
) {
|
|
540
|
+
return null
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
cursor = subrecord.nextOffset
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return cursor
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Finds the next plausible known primitive record after optional extra
|
|
551
|
+
* subrecords attached to the current primitive.
|
|
552
|
+
* @param {Uint8Array} bytes
|
|
553
|
+
* @param {number} offset
|
|
554
|
+
* @returns {number | null}
|
|
555
|
+
*/
|
|
556
|
+
static #findNextKnownRecordOffset(bytes, offset) {
|
|
557
|
+
let cursor = offset
|
|
558
|
+
|
|
559
|
+
while (cursor < bytes.byteLength) {
|
|
560
|
+
if (PcbLibStreamExtractor.#isKnownRecordStart(bytes, cursor)) {
|
|
561
|
+
return cursor
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const unknownSubrecord = PcbLibStreamExtractor.#readSubrecordAt(
|
|
565
|
+
bytes,
|
|
566
|
+
cursor
|
|
567
|
+
)
|
|
568
|
+
if (unknownSubrecord && unknownSubrecord.nextOffset > cursor) {
|
|
569
|
+
cursor = unknownSubrecord.nextOffset
|
|
570
|
+
continue
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
cursor += 1
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return null
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Returns true when an offset can start a known footprint primitive record.
|
|
581
|
+
* @param {Uint8Array} bytes
|
|
582
|
+
* @param {number} offset
|
|
583
|
+
* @returns {boolean}
|
|
584
|
+
*/
|
|
585
|
+
static #isKnownRecordStart(bytes, offset) {
|
|
586
|
+
const descriptor =
|
|
587
|
+
PcbLibStreamExtractor.#FOOTPRINT_RECORD_TYPES[bytes[offset]]
|
|
588
|
+
|
|
589
|
+
return Boolean(
|
|
590
|
+
descriptor &&
|
|
591
|
+
PcbLibStreamExtractor.#readMinimumRecordEnd(
|
|
592
|
+
bytes,
|
|
593
|
+
offset,
|
|
594
|
+
descriptor
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Appends one parsed record to the appropriate footprint primitive list.
|
|
601
|
+
* @param {object} collections
|
|
602
|
+
* @param {{ typeId: number, descriptor: object | null, recordBytes: Uint8Array, offset: number, byteLength: number }} record
|
|
603
|
+
* @param {Record<number, string>} wideStrings
|
|
604
|
+
* @param {string} sourceStorage
|
|
605
|
+
* @param {number} recordIndex
|
|
606
|
+
*/
|
|
607
|
+
static #appendFootprintRecord(
|
|
608
|
+
collections,
|
|
609
|
+
record,
|
|
610
|
+
wideStrings,
|
|
611
|
+
sourceStorage,
|
|
612
|
+
recordIndex
|
|
613
|
+
) {
|
|
614
|
+
if (!record.descriptor) {
|
|
615
|
+
collections.rawRecords.push(
|
|
616
|
+
PcbRawRecordRegistry.createPcbLibRecord({
|
|
617
|
+
sourceStorage,
|
|
618
|
+
record,
|
|
619
|
+
recordIndex,
|
|
620
|
+
parsed: false
|
|
621
|
+
})
|
|
622
|
+
)
|
|
623
|
+
collections.unknownRecords.push({
|
|
624
|
+
typeId: record.typeId,
|
|
625
|
+
offset: record.offset,
|
|
626
|
+
byteLength: record.byteLength
|
|
627
|
+
})
|
|
628
|
+
return
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const primitives = PcbLibStreamExtractor.#parsePrimitiveRecord(
|
|
632
|
+
record,
|
|
633
|
+
wideStrings
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
if (!primitives.length) {
|
|
637
|
+
collections.rawRecords.push(
|
|
638
|
+
PcbRawRecordRegistry.createPcbLibRecord({
|
|
639
|
+
sourceStorage,
|
|
640
|
+
record,
|
|
641
|
+
recordIndex,
|
|
642
|
+
parsed: false
|
|
643
|
+
})
|
|
644
|
+
)
|
|
645
|
+
collections.unknownRecords.push({
|
|
646
|
+
typeId: record.typeId,
|
|
647
|
+
offset: record.offset,
|
|
648
|
+
byteLength: record.byteLength
|
|
649
|
+
})
|
|
650
|
+
return
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
collections.rawRecords.push(
|
|
654
|
+
PcbRawRecordRegistry.createPcbLibRecord({
|
|
655
|
+
sourceStorage,
|
|
656
|
+
record,
|
|
657
|
+
recordIndex,
|
|
658
|
+
parsed: true
|
|
659
|
+
})
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
for (const primitive of primitives) {
|
|
663
|
+
const collection = collections[record.descriptor.collection]
|
|
664
|
+
const index = collection.length
|
|
665
|
+
collection.push(primitive)
|
|
666
|
+
collections.primitiveOrder.push({
|
|
667
|
+
type: record.descriptor.type,
|
|
668
|
+
collection: record.descriptor.collection,
|
|
669
|
+
index
|
|
670
|
+
})
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Parses one primitive record using the existing PcbDoc binary primitive
|
|
676
|
+
* parsers with an artificial one-record header.
|
|
677
|
+
* @param {{ descriptor: { parser: string }, recordBytes: Uint8Array }} record
|
|
678
|
+
* @param {Record<number, string>} wideStrings
|
|
679
|
+
* @returns {object[]}
|
|
680
|
+
*/
|
|
681
|
+
static #parsePrimitiveRecord(record, wideStrings) {
|
|
682
|
+
const headerBytes = PcbLibStreamExtractor.#createCountHeader(1)
|
|
683
|
+
const parser = PcbBinaryPrimitiveParser[record.descriptor.parser]
|
|
684
|
+
const primitives = parser.call(
|
|
685
|
+
PcbBinaryPrimitiveParser,
|
|
686
|
+
headerBytes,
|
|
687
|
+
record.recordBytes
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
if (record.descriptor.type === 'text') {
|
|
691
|
+
return primitives.map((primitive) =>
|
|
692
|
+
PcbLibStreamExtractor.#resolveTextWideString(
|
|
693
|
+
primitive,
|
|
694
|
+
wideStrings
|
|
695
|
+
)
|
|
696
|
+
)
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return primitives
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Resolves text through the footprint WideStrings table when the parser
|
|
704
|
+
* exposes a numeric text index.
|
|
705
|
+
* @param {object} primitive
|
|
706
|
+
* @param {Record<number, string>} wideStrings
|
|
707
|
+
* @returns {object}
|
|
708
|
+
*/
|
|
709
|
+
static #resolveTextWideString(primitive, wideStrings) {
|
|
710
|
+
const wideText = wideStrings[primitive.wideStringIndex]
|
|
711
|
+
|
|
712
|
+
return wideText ? { ...primitive, text: wideText } : primitive
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Parses one length-prefixed property stream.
|
|
717
|
+
* @param {Uint8Array | undefined} bytes
|
|
718
|
+
* @returns {Record<string, string>}
|
|
719
|
+
*/
|
|
720
|
+
static #parsePropertyStream(bytes) {
|
|
721
|
+
if (!bytes) {
|
|
722
|
+
return {}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const record = PcbLibStreamExtractor.#readLengthPrefixedTextAt(bytes, 0)
|
|
726
|
+
|
|
727
|
+
return record
|
|
728
|
+
? PcbLibStreamExtractor.#parsePipeProperties(record.text)
|
|
729
|
+
: {}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Parses one PcbLib WideStrings stream.
|
|
734
|
+
* @param {Uint8Array | undefined} bytes
|
|
735
|
+
* @returns {Record<number, string>}
|
|
736
|
+
*/
|
|
737
|
+
static #parseWideStrings(bytes) {
|
|
738
|
+
const properties = PcbLibStreamExtractor.#parsePropertyStream(bytes)
|
|
739
|
+
const wideStrings = {}
|
|
740
|
+
|
|
741
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
742
|
+
const match = key.match(/^ENCODEDTEXT(\d+)$/u)
|
|
743
|
+
if (!match) {
|
|
744
|
+
continue
|
|
745
|
+
}
|
|
746
|
+
wideStrings[Number(match[1])] =
|
|
747
|
+
PcbLibStreamExtractor.#decodeCsvCharCodes(value)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return wideStrings
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Parses pipe-delimited Altium properties.
|
|
755
|
+
* @param {string} text
|
|
756
|
+
* @returns {Record<string, string>}
|
|
757
|
+
*/
|
|
758
|
+
static #parsePipeProperties(text) {
|
|
759
|
+
const properties = {}
|
|
760
|
+
|
|
761
|
+
for (const part of String(text || '')
|
|
762
|
+
.replace(/\u0000+$/u, '')
|
|
763
|
+
.split('|')) {
|
|
764
|
+
const separator = part.indexOf('=')
|
|
765
|
+
if (separator <= 0) {
|
|
766
|
+
continue
|
|
767
|
+
}
|
|
768
|
+
properties[part.slice(0, separator)] = part
|
|
769
|
+
.slice(separator + 1)
|
|
770
|
+
.replace(/\r?\n$/u, '')
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return properties
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Decodes comma-separated character codes into text.
|
|
778
|
+
* @param {string} value
|
|
779
|
+
* @returns {string}
|
|
780
|
+
*/
|
|
781
|
+
static #decodeCsvCharCodes(value) {
|
|
782
|
+
return String(value || '')
|
|
783
|
+
.split(',')
|
|
784
|
+
.filter(Boolean)
|
|
785
|
+
.map((part) => String.fromCharCode(Number(part)))
|
|
786
|
+
.join('')
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Reads one length-prefixed UTF-8/ASCII text block.
|
|
791
|
+
* @param {Uint8Array} bytes
|
|
792
|
+
* @param {number} offset
|
|
793
|
+
* @returns {{ text: string, nextOffset: number } | null}
|
|
794
|
+
*/
|
|
795
|
+
static #readLengthPrefixedTextAt(bytes, offset) {
|
|
796
|
+
if (!bytes || offset + 4 > bytes.byteLength) {
|
|
797
|
+
return null
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const byteLength = PcbLibStreamExtractor.#readUint32(bytes, offset)
|
|
801
|
+
const payloadOffset = offset + 4
|
|
802
|
+
const nextOffset = payloadOffset + byteLength
|
|
803
|
+
|
|
804
|
+
if (nextOffset > bytes.byteLength) {
|
|
805
|
+
return null
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return {
|
|
809
|
+
text: new TextDecoder().decode(
|
|
810
|
+
bytes.slice(payloadOffset, nextOffset)
|
|
811
|
+
),
|
|
812
|
+
nextOffset
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Reads one PCB string block with a four-byte block length and Pascal text
|
|
818
|
+
* payload.
|
|
819
|
+
* @param {Uint8Array} bytes
|
|
820
|
+
* @param {number} offset
|
|
821
|
+
* @returns {{ text: string, nextOffset: number } | null}
|
|
822
|
+
*/
|
|
823
|
+
static #readStringBlockAt(bytes, offset) {
|
|
824
|
+
if (!bytes || offset + 5 > bytes.byteLength) {
|
|
825
|
+
return null
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const blockByteLength = PcbLibStreamExtractor.#readUint32(bytes, offset)
|
|
829
|
+
const payloadOffset = offset + 4
|
|
830
|
+
const nextOffset = payloadOffset + blockByteLength
|
|
831
|
+
|
|
832
|
+
if (blockByteLength < 1 || nextOffset > bytes.byteLength) {
|
|
833
|
+
return null
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const textByteLength = Math.min(
|
|
837
|
+
bytes[payloadOffset],
|
|
838
|
+
blockByteLength - 1
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
return {
|
|
842
|
+
text: new TextDecoder().decode(
|
|
843
|
+
bytes.slice(
|
|
844
|
+
payloadOffset + 1,
|
|
845
|
+
payloadOffset + 1 + textByteLength
|
|
846
|
+
)
|
|
847
|
+
),
|
|
848
|
+
nextOffset
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Reads one length-prefixed primitive subrecord at an offset.
|
|
854
|
+
* @param {Uint8Array} bytes
|
|
855
|
+
* @param {number} offset
|
|
856
|
+
* @returns {{ payloadByteLength: number, payloadOffset: number, nextOffset: number } | null}
|
|
857
|
+
*/
|
|
858
|
+
static #readSubrecordAt(bytes, offset) {
|
|
859
|
+
if (offset + 4 > bytes.byteLength) {
|
|
860
|
+
return null
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const payloadByteLength = PcbLibStreamExtractor.#readUint32(
|
|
864
|
+
bytes,
|
|
865
|
+
offset
|
|
866
|
+
)
|
|
867
|
+
const payloadOffset = offset + 4
|
|
868
|
+
const nextOffset = payloadOffset + payloadByteLength
|
|
869
|
+
|
|
870
|
+
if (nextOffset > bytes.byteLength) {
|
|
871
|
+
return null
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return { payloadByteLength, payloadOffset, nextOffset }
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Reads one count from a four-byte Header stream.
|
|
879
|
+
* @param {Uint8Array | undefined} bytes
|
|
880
|
+
* @returns {number}
|
|
881
|
+
*/
|
|
882
|
+
static #readCountHeader(bytes) {
|
|
883
|
+
if (!bytes || bytes.byteLength < 4) {
|
|
884
|
+
return 0
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
return PcbLibStreamExtractor.#readUint32(bytes, 0)
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Creates one little-endian four-byte count header.
|
|
892
|
+
* @param {number} count
|
|
893
|
+
* @returns {Uint8Array}
|
|
894
|
+
*/
|
|
895
|
+
static #createCountHeader(count) {
|
|
896
|
+
const bytes = new Uint8Array(4)
|
|
897
|
+
new DataView(bytes.buffer).setUint32(0, count, true)
|
|
898
|
+
return bytes
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Returns all stream names that contributed to the extraction.
|
|
903
|
+
* @param {object[]} footprints
|
|
904
|
+
* @param {Map<string, Uint8Array>} streams
|
|
905
|
+
* @param {{ fonts?: { sourceStream: string }[] }} embeddedFonts
|
|
906
|
+
* @returns {string[]}
|
|
907
|
+
*/
|
|
908
|
+
static #collectUsedStreamNames(footprints, streams, embeddedFonts) {
|
|
909
|
+
const names = new Set()
|
|
910
|
+
|
|
911
|
+
for (const baseName of [
|
|
912
|
+
'Library/Data',
|
|
913
|
+
'Library/ComponentParamsTOC/Data',
|
|
914
|
+
'SectionKeys'
|
|
915
|
+
]) {
|
|
916
|
+
if (streams.has(baseName)) {
|
|
917
|
+
names.add(baseName)
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
for (const footprint of footprints) {
|
|
922
|
+
for (const suffix of [
|
|
923
|
+
'Header',
|
|
924
|
+
'Data',
|
|
925
|
+
'Parameters',
|
|
926
|
+
'WideStrings'
|
|
927
|
+
]) {
|
|
928
|
+
const name = footprint.sourceStorage + '/' + suffix
|
|
929
|
+
if (streams.has(name)) {
|
|
930
|
+
names.add(name)
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
for (const font of embeddedFonts.fonts || []) {
|
|
936
|
+
if (streams.has(font.sourceStream)) {
|
|
937
|
+
names.add(font.sourceStream)
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
return [...names].sort((left, right) => left.localeCompare(right))
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Sanitizes one footprint name for legacy OLE storage lookup.
|
|
946
|
+
* @param {string} name
|
|
947
|
+
* @returns {string}
|
|
948
|
+
*/
|
|
949
|
+
static #sanitizeStorageName(name) {
|
|
950
|
+
return String(name || '')
|
|
951
|
+
.replace(/[/:\\]/gu, '_')
|
|
952
|
+
.slice(0, 31)
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Reads a little-endian unsigned 32-bit value from one byte array.
|
|
957
|
+
* @param {Uint8Array} bytes
|
|
958
|
+
* @param {number} offset
|
|
959
|
+
* @returns {number}
|
|
960
|
+
*/
|
|
961
|
+
static #readUint32(bytes, offset) {
|
|
962
|
+
return new DataView(
|
|
963
|
+
bytes.buffer,
|
|
964
|
+
bytes.byteOffset + offset,
|
|
965
|
+
4
|
|
966
|
+
).getUint32(0, true)
|
|
967
|
+
}
|
|
968
|
+
}
|