altium-toolkit 1.1.2 → 1.1.22
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/docs/api.md +37 -0
- package/docs/model-format.md +18 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +2 -2
- package/docs/testing.md +5 -0
- package/package.json +1 -1
- package/spec/library-scope.md +5 -0
- package/src/core/altium/AltiumLayoutParser.mjs +275 -13
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +245 -10
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AsciiRecordParser.mjs +43 -11
- package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
- package/src/core/altium/PrintableTextDecoder.mjs +133 -13
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +13 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +40 -1
- package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
- package/src/core/altium/SchematicImageParser.mjs +291 -6
- package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
- package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
- package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicPinParser.mjs +262 -24
- package/src/core/altium/SchematicPrimitiveParser.mjs +116 -8
- package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +62 -15
- package/src/core/altium/SchematicTextParser.mjs +125 -11
- package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
- package/src/core/altium/SourceBundleExporter.mjs +156 -0
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
- package/src/core/altium/SourceComponentClient.mjs +239 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +8 -0
- package/src/styles/altium-renderers.css +6 -6
- package/src/ui/PcbArcUtils.mjs +19 -2
- package/src/ui/PcbScene3dBuilder.mjs +202 -20
- package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
- package/src/ui/SchematicColorResolver.mjs +263 -0
- package/src/ui/SchematicContentLayout.mjs +58 -1
- package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
- package/src/ui/SchematicImageRenderer.mjs +125 -10
- package/src/ui/SchematicJunctionRenderer.mjs +1 -1
- package/src/ui/SchematicLineColorResolver.mjs +88 -0
- package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
- package/src/ui/SchematicNoteRenderer.mjs +87 -7
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +560 -10
- package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +397 -48
- package/src/ui/SchematicPowerDiagramImageProcessor.mjs +970 -0
- package/src/ui/SchematicPowerDiagramLineMasks.mjs +631 -0
- package/src/ui/SchematicPowerPortRenderer.mjs +1 -1
- package/src/ui/SchematicShapeRenderer.mjs +109 -24
- package/src/ui/SchematicSvgRenderer.mjs +1210 -71
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { OleCompoundDocumentWriter } from '../ole/OleCompoundDocumentWriter.mjs'
|
|
6
|
+
import { AltiumLibraryRecordBuilder } from './AltiumLibraryRecordBuilder.mjs'
|
|
7
|
+
import { SourceComponentBundleNormalizer } from './SourceComponentBundleNormalizer.mjs'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Exports normalized bundles into a compact OLE-backed footprint library.
|
|
11
|
+
*/
|
|
12
|
+
export class AltiumPcbLibExporter {
|
|
13
|
+
/**
|
|
14
|
+
* Exports one or more component bundles as `.PcbLib` bytes.
|
|
15
|
+
* @param {object[] | object} bundles Component bundles.
|
|
16
|
+
* @returns {Uint8Array}
|
|
17
|
+
*/
|
|
18
|
+
static export(bundles) {
|
|
19
|
+
const normalizedBundles =
|
|
20
|
+
AltiumPcbLibExporter.#normalizeBundles(bundles)
|
|
21
|
+
const streams = new Map()
|
|
22
|
+
const modelRows = AltiumPcbLibExporter.#collectModels(normalizedBundles)
|
|
23
|
+
|
|
24
|
+
streams.set(
|
|
25
|
+
'Library/Data',
|
|
26
|
+
AltiumLibraryRecordBuilder.buildPcbLibraryData(normalizedBundles)
|
|
27
|
+
)
|
|
28
|
+
streams.set(
|
|
29
|
+
'Library/ComponentParamsTOC/Data',
|
|
30
|
+
AltiumLibraryRecordBuilder.buildComponentParamsToc(
|
|
31
|
+
normalizedBundles
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
streams.set(
|
|
35
|
+
'SectionKeys',
|
|
36
|
+
AltiumLibraryRecordBuilder.buildSectionKeys(normalizedBundles)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
for (const bundle of normalizedBundles) {
|
|
40
|
+
const storageName = AltiumLibraryRecordBuilder.sanitizeStorageName(
|
|
41
|
+
bundle.footprint.name
|
|
42
|
+
)
|
|
43
|
+
streams.set(
|
|
44
|
+
storageName + '/Header',
|
|
45
|
+
AltiumLibraryRecordBuilder.createCountHeader(0)
|
|
46
|
+
)
|
|
47
|
+
streams.set(
|
|
48
|
+
storageName + '/Parameters',
|
|
49
|
+
AltiumLibraryRecordBuilder.buildFootprintParameters(bundle)
|
|
50
|
+
)
|
|
51
|
+
streams.set(
|
|
52
|
+
storageName + '/Data',
|
|
53
|
+
AltiumLibraryRecordBuilder.buildFootprintData(bundle)
|
|
54
|
+
)
|
|
55
|
+
streams.set(
|
|
56
|
+
storageName + '/SourceRecord',
|
|
57
|
+
new TextEncoder().encode(
|
|
58
|
+
AltiumLibraryRecordBuilder.buildPcbFootprintRecord(bundle)
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (modelRows.length) {
|
|
64
|
+
streams.set(
|
|
65
|
+
'Models/Data',
|
|
66
|
+
AltiumLibraryRecordBuilder.buildModelsData(modelRows)
|
|
67
|
+
)
|
|
68
|
+
modelRows.forEach((row, index) => {
|
|
69
|
+
streams.set('Models/' + index, row.model.bytes)
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return OleCompoundDocumentWriter.write({ streams })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Collects model rows with deterministic generated ids.
|
|
78
|
+
* @param {object[]} bundles Normalized bundles.
|
|
79
|
+
* @returns {{ model: object, id: string, checksum: number }[]}
|
|
80
|
+
*/
|
|
81
|
+
static #collectModels(bundles) {
|
|
82
|
+
return bundles.flatMap((bundle, bundleIndex) =>
|
|
83
|
+
bundle.models.map((model, modelIndex) => ({
|
|
84
|
+
model,
|
|
85
|
+
id: 'model-' + bundleIndex + '-' + modelIndex,
|
|
86
|
+
checksum: AltiumLibraryRecordBuilder.checksumBytes(model.bytes)
|
|
87
|
+
}))
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Normalizes one or more bundles.
|
|
93
|
+
* @param {object[] | object} bundles Bundle input.
|
|
94
|
+
* @returns {object[]}
|
|
95
|
+
*/
|
|
96
|
+
static #normalizeBundles(bundles) {
|
|
97
|
+
return (Array.isArray(bundles) ? bundles : [bundles]).map((bundle) =>
|
|
98
|
+
SourceComponentBundleNormalizer.normalize(bundle)
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { OleCompoundDocumentWriter } from '../ole/OleCompoundDocumentWriter.mjs'
|
|
6
|
+
import { AltiumLibraryRecordBuilder } from './AltiumLibraryRecordBuilder.mjs'
|
|
7
|
+
import { SourceComponentBundleNormalizer } from './SourceComponentBundleNormalizer.mjs'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Exports normalized bundles into a compact OLE-backed schematic library.
|
|
11
|
+
*/
|
|
12
|
+
export class AltiumSchLibExporter {
|
|
13
|
+
/**
|
|
14
|
+
* Exports one or more component bundles as `.SchLib` bytes.
|
|
15
|
+
* @param {object[] | object} bundles Component bundles.
|
|
16
|
+
* @returns {Uint8Array}
|
|
17
|
+
*/
|
|
18
|
+
static export(bundles) {
|
|
19
|
+
const normalizedBundles =
|
|
20
|
+
AltiumSchLibExporter.#normalizeBundles(bundles)
|
|
21
|
+
const streams = new Map()
|
|
22
|
+
const libraryRecord = normalizedBundles
|
|
23
|
+
.map((bundle) =>
|
|
24
|
+
AltiumLibraryRecordBuilder.buildSchematicComponentRecord(bundle)
|
|
25
|
+
)
|
|
26
|
+
.join('\n')
|
|
27
|
+
|
|
28
|
+
streams.set('Library/Data', new TextEncoder().encode(libraryRecord))
|
|
29
|
+
for (const bundle of normalizedBundles) {
|
|
30
|
+
streams.set(
|
|
31
|
+
'Components/' +
|
|
32
|
+
AltiumLibraryRecordBuilder.sanitizeStorageName(
|
|
33
|
+
bundle.symbol.name
|
|
34
|
+
) +
|
|
35
|
+
'/Data',
|
|
36
|
+
new TextEncoder().encode(
|
|
37
|
+
AltiumLibraryRecordBuilder.buildSchematicComponentRecord(
|
|
38
|
+
bundle
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return OleCompoundDocumentWriter.write({ streams })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Normalizes one or more bundles.
|
|
49
|
+
* @param {object[] | object} bundles Bundle input.
|
|
50
|
+
* @returns {object[]}
|
|
51
|
+
*/
|
|
52
|
+
static #normalizeBundles(bundles) {
|
|
53
|
+
return (Array.isArray(bundles) ? bundles : [bundles]).map((bundle) =>
|
|
54
|
+
SourceComponentBundleNormalizer.normalize(bundle)
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -146,17 +146,11 @@ export class AsciiRecordParser {
|
|
|
146
146
|
const rawKey = AsciiRecordParser.#trimAscii(
|
|
147
147
|
segment.slice(0, separatorIndex)
|
|
148
148
|
)
|
|
149
|
-
const value = PrintableTextDecoder.decodeBytes(
|
|
150
|
-
AsciiRecordParser.#binaryStringToBytes(
|
|
151
|
-
AsciiRecordParser.#trimAscii(
|
|
152
|
-
segment.slice(separatorIndex + 1)
|
|
153
|
-
)
|
|
154
|
-
),
|
|
155
|
-
{
|
|
156
|
-
encoding: rawKey.startsWith('%UTF8%') ? 'utf-8' : undefined
|
|
157
|
-
}
|
|
158
|
-
)
|
|
159
149
|
const isUtf8Field = rawKey.startsWith('%UTF8%')
|
|
150
|
+
const value = AsciiRecordParser.#decodeFieldValue(
|
|
151
|
+
AsciiRecordParser.#trimAscii(segment.slice(separatorIndex + 1)),
|
|
152
|
+
isUtf8Field ? 'utf-8' : ''
|
|
153
|
+
)
|
|
160
154
|
const key = rawKey.replace(/^%UTF8%/, '')
|
|
161
155
|
if (!key) continue
|
|
162
156
|
|
|
@@ -228,13 +222,51 @@ export class AsciiRecordParser {
|
|
|
228
222
|
return normalizedKeyIndex
|
|
229
223
|
}
|
|
230
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Decodes one pipe-delimited field value from the byte-preserving run
|
|
227
|
+
* string. Plain ASCII is already decoded by construction and can avoid
|
|
228
|
+
* byte-array allocation plus TextDecoder fallback probing.
|
|
229
|
+
* @param {string} value Byte-preserving field value.
|
|
230
|
+
* @param {string} preferredEncoding Optional preferred decoder encoding.
|
|
231
|
+
* @returns {string}
|
|
232
|
+
*/
|
|
233
|
+
static #decodeFieldValue(value, preferredEncoding) {
|
|
234
|
+
if (!AsciiRecordParser.#hasExtendedByte(value)) {
|
|
235
|
+
return value
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return PrintableTextDecoder.decodeBytes(
|
|
239
|
+
AsciiRecordParser.#binaryStringToBytes(value),
|
|
240
|
+
{ encoding: preferredEncoding || undefined }
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
231
244
|
/**
|
|
232
245
|
* Converts one binary string into bytes without altering byte values.
|
|
233
246
|
* @param {string} value
|
|
234
247
|
* @returns {Uint8Array}
|
|
235
248
|
*/
|
|
236
249
|
static #binaryStringToBytes(value) {
|
|
237
|
-
|
|
250
|
+
const bytes = new Uint8Array(value.length)
|
|
251
|
+
|
|
252
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
253
|
+
bytes[index] = value.charCodeAt(index) & 0xff
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return bytes
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Returns true when the byte-preserving string contains non-ASCII bytes.
|
|
261
|
+
* @param {string} value Field value.
|
|
262
|
+
* @returns {boolean}
|
|
263
|
+
*/
|
|
264
|
+
static #hasExtendedByte(value) {
|
|
265
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
266
|
+
if (value.charCodeAt(index) > 0x7f) return true
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return false
|
|
238
270
|
}
|
|
239
271
|
|
|
240
272
|
/**
|
|
@@ -12,49 +12,49 @@ const KIND_BY_VALUE = {
|
|
|
12
12
|
displayName: 'Standard',
|
|
13
13
|
includeInBom: true,
|
|
14
14
|
includeInNetlist: true,
|
|
15
|
-
|
|
15
|
+
includeInPlacement: true
|
|
16
16
|
},
|
|
17
17
|
1: {
|
|
18
18
|
name: 'mechanical',
|
|
19
19
|
displayName: 'Mechanical',
|
|
20
20
|
includeInBom: true,
|
|
21
21
|
includeInNetlist: true,
|
|
22
|
-
|
|
22
|
+
includeInPlacement: true
|
|
23
23
|
},
|
|
24
24
|
2: {
|
|
25
25
|
name: 'graphical',
|
|
26
26
|
displayName: 'Graphical',
|
|
27
27
|
includeInBom: false,
|
|
28
28
|
includeInNetlist: false,
|
|
29
|
-
|
|
29
|
+
includeInPlacement: false
|
|
30
30
|
},
|
|
31
31
|
3: {
|
|
32
32
|
name: 'net-tie-bom',
|
|
33
33
|
displayName: 'Net Tie BOM',
|
|
34
34
|
includeInBom: true,
|
|
35
35
|
includeInNetlist: true,
|
|
36
|
-
|
|
36
|
+
includeInPlacement: true
|
|
37
37
|
},
|
|
38
38
|
4: {
|
|
39
39
|
name: 'net-tie-no-bom',
|
|
40
40
|
displayName: 'Net Tie No BOM',
|
|
41
41
|
includeInBom: false,
|
|
42
42
|
includeInNetlist: true,
|
|
43
|
-
|
|
43
|
+
includeInPlacement: true
|
|
44
44
|
},
|
|
45
45
|
5: {
|
|
46
46
|
name: 'standard-no-bom',
|
|
47
47
|
displayName: 'Standard No BOM',
|
|
48
48
|
includeInBom: false,
|
|
49
49
|
includeInNetlist: true,
|
|
50
|
-
|
|
50
|
+
includeInPlacement: true
|
|
51
51
|
},
|
|
52
52
|
6: {
|
|
53
53
|
name: 'jumper',
|
|
54
54
|
displayName: 'Jumper',
|
|
55
55
|
includeInBom: true,
|
|
56
56
|
includeInNetlist: true,
|
|
57
|
-
|
|
57
|
+
includeInPlacement: true
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -65,7 +65,7 @@ export class PcbComponentKindPolicy {
|
|
|
65
65
|
/**
|
|
66
66
|
* Parses native versioned component-kind fields.
|
|
67
67
|
* @param {Record<string, string | string[]>} fields Native component row.
|
|
68
|
-
* @returns {{ value: number, name: string, displayName: string, includeInBom: boolean, includeInNetlist: boolean,
|
|
68
|
+
* @returns {{ value: number, name: string, displayName: string, includeInBom: boolean, includeInNetlist: boolean, includeInPlacement: boolean } | undefined}
|
|
69
69
|
*/
|
|
70
70
|
static parse(fields) {
|
|
71
71
|
if (!PcbComponentKindPolicy.#hasKindField(fields)) return undefined
|
|
@@ -76,7 +76,7 @@ export class PcbComponentKindPolicy {
|
|
|
76
76
|
displayName: 'Unknown',
|
|
77
77
|
includeInBom: true,
|
|
78
78
|
includeInNetlist: true,
|
|
79
|
-
|
|
79
|
+
includeInPlacement: true
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
return {
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Unzlib } from 'fflate'
|
|
6
6
|
import { PcbFontMetricsParser } from './PcbFontMetricsParser.mjs'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Extracts zlib-compressed embedded font payloads from PCB compound streams.
|
|
10
10
|
*/
|
|
11
11
|
export class PcbEmbeddedFontExtractor {
|
|
12
|
+
static #BASE64_ALPHABET =
|
|
13
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
14
|
+
|
|
12
15
|
static #CANDIDATE_STREAM_NAMES = [
|
|
13
16
|
'EmbeddedFonts6/Data',
|
|
14
17
|
'EmbeddedFonts/Data',
|
|
@@ -114,16 +117,19 @@ export class PcbEmbeddedFontExtractor {
|
|
|
114
117
|
return null
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
const
|
|
120
|
+
const payload = PcbEmbeddedFontExtractor.#inflateZlibPayloadAt(
|
|
118
121
|
bytes,
|
|
119
122
|
zlibOffset
|
|
120
123
|
)
|
|
121
|
-
if (
|
|
124
|
+
if (!payload) {
|
|
122
125
|
return null
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
const compressedBytes = bytes.subarray(
|
|
126
|
-
|
|
128
|
+
const compressedBytes = bytes.subarray(
|
|
129
|
+
zlibOffset,
|
|
130
|
+
payload.compressedEnd
|
|
131
|
+
)
|
|
132
|
+
const payloadBytes = payload.bytes
|
|
127
133
|
const metadata = PcbEmbeddedFontExtractor.#normalizeFontMetadata(
|
|
128
134
|
familyField.text,
|
|
129
135
|
alternateField.text,
|
|
@@ -151,7 +157,7 @@ export class PcbEmbeddedFontExtractor {
|
|
|
151
157
|
PcbEmbeddedFontExtractor.#bytesToBase64(payloadBytes),
|
|
152
158
|
metrics
|
|
153
159
|
},
|
|
154
|
-
nextOffset: compressedEnd
|
|
160
|
+
nextOffset: payload.compressedEnd
|
|
155
161
|
}
|
|
156
162
|
}
|
|
157
163
|
|
|
@@ -224,48 +230,146 @@ export class PcbEmbeddedFontExtractor {
|
|
|
224
230
|
}
|
|
225
231
|
|
|
226
232
|
/**
|
|
227
|
-
*
|
|
233
|
+
* Inflates one zlib payload and returns its exact stream boundary.
|
|
228
234
|
* @param {Uint8Array} bytes
|
|
229
235
|
* @param {number} zlibOffset
|
|
236
|
+
* @returns {{ bytes: Uint8Array, compressedEnd: number } | null}
|
|
237
|
+
*/
|
|
238
|
+
static #inflateZlibPayloadAt(bytes, zlibOffset) {
|
|
239
|
+
const input = bytes.subarray(zlibOffset)
|
|
240
|
+
const chunks = []
|
|
241
|
+
let inflater
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
inflater = new Unzlib((chunk) => {
|
|
245
|
+
chunks.push(chunk)
|
|
246
|
+
})
|
|
247
|
+
inflater.push(input, false)
|
|
248
|
+
} catch {
|
|
249
|
+
return null
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!Number(inflater?.s?.f || 0)) {
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const payloadBytes = PcbEmbeddedFontExtractor.#concatBytes(chunks)
|
|
257
|
+
const compressedByteCount =
|
|
258
|
+
PcbEmbeddedFontExtractor.#resolveCompressedByteCount(
|
|
259
|
+
input,
|
|
260
|
+
inflater,
|
|
261
|
+
payloadBytes
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if (compressedByteCount <= 2) {
|
|
265
|
+
return null
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
bytes: payloadBytes,
|
|
270
|
+
compressedEnd: zlibOffset + compressedByteCount
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Resolves the zlib stream length from fflate's remaining input buffer.
|
|
276
|
+
* @param {Uint8Array} input
|
|
277
|
+
* @param {Unzlib} inflater
|
|
278
|
+
* @param {Uint8Array} payloadBytes
|
|
230
279
|
* @returns {number}
|
|
231
280
|
*/
|
|
232
|
-
static #
|
|
233
|
-
|
|
234
|
-
|
|
281
|
+
static #resolveCompressedByteCount(input, inflater, payloadBytes) {
|
|
282
|
+
const remainingByteCount = Number(inflater?.p?.byteLength || 0)
|
|
283
|
+
if (remainingByteCount < 4) {
|
|
284
|
+
return -1
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const baseByteCount = input.byteLength - remainingByteCount + 4
|
|
288
|
+
const checksum = PcbEmbeddedFontExtractor.#adler32(payloadBytes)
|
|
235
289
|
|
|
236
|
-
|
|
237
|
-
|
|
290
|
+
// fflate can leave the final consumed deflate byte in `p` when the
|
|
291
|
+
// stream ends mid-byte, so validate both adjacent boundary candidates.
|
|
292
|
+
for (const compressedByteCount of [baseByteCount, baseByteCount + 1]) {
|
|
238
293
|
if (
|
|
239
|
-
PcbEmbeddedFontExtractor.#
|
|
240
|
-
|
|
294
|
+
PcbEmbeddedFontExtractor.#hasZlibChecksumAt(
|
|
295
|
+
input,
|
|
296
|
+
compressedByteCount,
|
|
297
|
+
checksum
|
|
241
298
|
)
|
|
242
299
|
) {
|
|
243
|
-
|
|
244
|
-
} else {
|
|
245
|
-
low = midpoint + 1
|
|
300
|
+
return compressedByteCount
|
|
246
301
|
}
|
|
247
302
|
}
|
|
248
303
|
|
|
249
|
-
return
|
|
250
|
-
bytes.subarray(zlibOffset, low)
|
|
251
|
-
)
|
|
252
|
-
? low
|
|
253
|
-
: -1
|
|
304
|
+
return -1
|
|
254
305
|
}
|
|
255
306
|
|
|
256
307
|
/**
|
|
257
|
-
* Returns true when
|
|
258
|
-
*
|
|
259
|
-
* @param {
|
|
308
|
+
* Returns true when a candidate zlib boundary ends with the checksum.
|
|
309
|
+
* @param {Uint8Array} input
|
|
310
|
+
* @param {number} compressedByteCount
|
|
311
|
+
* @param {number} checksum
|
|
260
312
|
* @returns {boolean}
|
|
261
313
|
*/
|
|
262
|
-
static #
|
|
263
|
-
|
|
264
|
-
unzlibSync(bytes)
|
|
265
|
-
return true
|
|
266
|
-
} catch {
|
|
314
|
+
static #hasZlibChecksumAt(input, compressedByteCount, checksum) {
|
|
315
|
+
if (compressedByteCount < 6 || compressedByteCount > input.byteLength) {
|
|
267
316
|
return false
|
|
268
317
|
}
|
|
318
|
+
|
|
319
|
+
const checksumOffset = compressedByteCount - 4
|
|
320
|
+
const actualChecksum = new DataView(
|
|
321
|
+
input.buffer,
|
|
322
|
+
input.byteOffset + checksumOffset,
|
|
323
|
+
4
|
|
324
|
+
).getUint32(0, false)
|
|
325
|
+
|
|
326
|
+
return actualChecksum === checksum
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Computes the Adler-32 checksum used by zlib trailers.
|
|
331
|
+
* @param {Uint8Array} bytes
|
|
332
|
+
* @returns {number}
|
|
333
|
+
*/
|
|
334
|
+
static #adler32(bytes) {
|
|
335
|
+
const modulo = 65521
|
|
336
|
+
let low = 1
|
|
337
|
+
let high = 0
|
|
338
|
+
|
|
339
|
+
for (let offset = 0; offset < bytes.byteLength; offset += 5552) {
|
|
340
|
+
const end = Math.min(offset + 5552, bytes.byteLength)
|
|
341
|
+
for (let index = offset; index < end; index += 1) {
|
|
342
|
+
low += bytes[index]
|
|
343
|
+
high += low
|
|
344
|
+
}
|
|
345
|
+
low %= modulo
|
|
346
|
+
high %= modulo
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return ((high << 16) | low) >>> 0
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Concatenates inflated output chunks.
|
|
354
|
+
* @param {Uint8Array[]} chunks
|
|
355
|
+
* @returns {Uint8Array}
|
|
356
|
+
*/
|
|
357
|
+
static #concatBytes(chunks) {
|
|
358
|
+
if (chunks.length === 1) {
|
|
359
|
+
return chunks[0]
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const bytes = new Uint8Array(
|
|
363
|
+
chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0)
|
|
364
|
+
)
|
|
365
|
+
let offset = 0
|
|
366
|
+
|
|
367
|
+
for (const chunk of chunks) {
|
|
368
|
+
bytes.set(chunk, offset)
|
|
369
|
+
offset += chunk.byteLength
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return bytes
|
|
269
373
|
}
|
|
270
374
|
|
|
271
375
|
/**
|
|
@@ -451,22 +555,61 @@ export class PcbEmbeddedFontExtractor {
|
|
|
451
555
|
* @returns {string}
|
|
452
556
|
*/
|
|
453
557
|
static #bytesToBase64(bytes) {
|
|
454
|
-
if (typeof
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
558
|
+
if (typeof Buffer === 'function' && typeof Buffer.from === 'function') {
|
|
559
|
+
return Buffer.from(bytes).toString('base64')
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return PcbEmbeddedFontExtractor.#bytesToBase64Portable(bytes)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Encodes bytes as base64 without relying on Node APIs.
|
|
567
|
+
* @param {Uint8Array} bytes
|
|
568
|
+
* @returns {string}
|
|
569
|
+
*/
|
|
570
|
+
static #bytesToBase64Portable(bytes) {
|
|
571
|
+
const alphabet = PcbEmbeddedFontExtractor.#BASE64_ALPHABET
|
|
572
|
+
const groupBuffer = new Array(4096)
|
|
573
|
+
const outputChunks = []
|
|
574
|
+
let groupIndex = 0
|
|
575
|
+
let byteIndex = 0
|
|
576
|
+
|
|
577
|
+
for (; byteIndex + 2 < bytes.byteLength; byteIndex += 3) {
|
|
578
|
+
const value =
|
|
579
|
+
(bytes[byteIndex] << 16) |
|
|
580
|
+
(bytes[byteIndex + 1] << 8) |
|
|
581
|
+
bytes[byteIndex + 2]
|
|
582
|
+
groupBuffer[groupIndex] =
|
|
583
|
+
alphabet[(value >> 18) & 63] +
|
|
584
|
+
alphabet[(value >> 12) & 63] +
|
|
585
|
+
alphabet[(value >> 6) & 63] +
|
|
586
|
+
alphabet[value & 63]
|
|
587
|
+
groupIndex += 1
|
|
588
|
+
|
|
589
|
+
if (groupIndex === groupBuffer.length) {
|
|
590
|
+
outputChunks.push(groupBuffer.join(''))
|
|
591
|
+
groupIndex = 0
|
|
465
592
|
}
|
|
466
|
-
return btoa(binary)
|
|
467
593
|
}
|
|
468
594
|
|
|
469
|
-
|
|
595
|
+
if (byteIndex < bytes.byteLength) {
|
|
596
|
+
const hasSecondByte = byteIndex + 1 < bytes.byteLength
|
|
597
|
+
const value =
|
|
598
|
+
(bytes[byteIndex] << 16) |
|
|
599
|
+
((hasSecondByte ? bytes[byteIndex + 1] : 0) << 8)
|
|
600
|
+
groupBuffer[groupIndex] =
|
|
601
|
+
alphabet[(value >> 18) & 63] +
|
|
602
|
+
alphabet[(value >> 12) & 63] +
|
|
603
|
+
(hasSecondByte ? alphabet[(value >> 6) & 63] : '=') +
|
|
604
|
+
'='
|
|
605
|
+
groupIndex += 1
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (groupIndex > 0) {
|
|
609
|
+
outputChunks.push(groupBuffer.slice(0, groupIndex).join(''))
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return outputChunks.join('')
|
|
470
613
|
}
|
|
471
614
|
|
|
472
615
|
/**
|
|
@@ -20,14 +20,16 @@ export class PcbEmbeddedModelExtractor {
|
|
|
20
20
|
* @returns {{ models: { id: string, checksum: number, name: string, format: string, payloadText: string, sourceStream: string, transform: { rotationDeg: { x: number, y: number, z: number }, dzMil: number } }[], componentBodies: { sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null }[] }}
|
|
21
21
|
*/
|
|
22
22
|
static extractFromStreams(streams) {
|
|
23
|
+
const modelStreamPrefix =
|
|
24
|
+
PcbEmbeddedModelExtractor.#resolveModelStreamPrefix(streams)
|
|
23
25
|
const modelMetadataRecords =
|
|
24
26
|
PcbEmbeddedModelExtractor.#parseModelMetadataStream(
|
|
25
|
-
streams.get('
|
|
27
|
+
streams.get(modelStreamPrefix + '/Data')
|
|
26
28
|
)
|
|
27
29
|
const modelMetadataRows = modelMetadataRecords.map((fields, index) => ({
|
|
28
30
|
fields,
|
|
29
31
|
index,
|
|
30
|
-
sourceStream: '
|
|
32
|
+
sourceStream: modelStreamPrefix + '/' + index,
|
|
31
33
|
id: PcbEmbeddedModelExtractor.#getField(fields, 'ID'),
|
|
32
34
|
name: PcbEmbeddedModelExtractor.#getField(fields, 'NAME'),
|
|
33
35
|
checksum: PcbEmbeddedModelExtractor.#normalizeChecksum(
|
|
@@ -70,7 +72,24 @@ export class PcbEmbeddedModelExtractor {
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
/**
|
|
73
|
-
*
|
|
75
|
+
* Resolves the embedded-model stream folder used by the compound document.
|
|
76
|
+
* @param {Map<string, Uint8Array>} streams
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
static #resolveModelStreamPrefix(streams) {
|
|
80
|
+
if (streams.has('Models/Data')) {
|
|
81
|
+
return 'Models'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (streams.has('Library/Models/Data')) {
|
|
85
|
+
return 'Library/Models'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return 'Models'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Parses the length-prefixed model metadata stream.
|
|
74
93
|
* @param {Uint8Array | undefined} bytes
|
|
75
94
|
* @returns {Record<string, string | string[]>[]}
|
|
76
95
|
*/
|