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
@@ -56,6 +56,12 @@ export class PcbViaStackParser {
56
56
 
57
57
  static #DRILL_LAYER_PAIR_TYPE_OFFSET = 317
58
58
 
59
+ static #PROPAGATION_DELAY_OFFSET = 321
60
+
61
+ static #HOLE_TOLERANCE_UNSET = 0x7fffffff
62
+
63
+ static #PICOSECONDS_PER_SECOND = 1_000_000_000_000
64
+
59
65
  static #PHYSICAL_LAYER_COUNT = 32
60
66
 
61
67
  /**
@@ -417,14 +423,21 @@ export class PcbViaStackParser {
417
423
  PcbViaStackParser.#TAIL_SIGNATURE_OFFSET + offsetShift,
418
424
  16
419
425
  )
420
- const positiveTolerance = PcbViaStackParser.#readMilIfAvailable(
421
- view,
422
- PcbViaStackParser.#POSITIVE_TOLERANCE_OFFSET + offsetShift
423
- )
424
- const negativeTolerance = PcbViaStackParser.#readMilIfAvailable(
425
- view,
426
- PcbViaStackParser.#NEGATIVE_TOLERANCE_OFFSET + offsetShift
427
- )
426
+ const positiveTolerance =
427
+ PcbViaStackParser.#readHoleToleranceIfAvailable(
428
+ view,
429
+ PcbViaStackParser.#POSITIVE_TOLERANCE_OFFSET + offsetShift
430
+ )
431
+ const negativeTolerance =
432
+ PcbViaStackParser.#readHoleToleranceIfAvailable(
433
+ view,
434
+ PcbViaStackParser.#NEGATIVE_TOLERANCE_OFFSET + offsetShift
435
+ )
436
+ const propagationDelayPs =
437
+ PcbViaStackParser.#readPropagationDelayIfAvailable(
438
+ view,
439
+ PcbViaStackParser.#PROPAGATION_DELAY_OFFSET + offsetShift
440
+ )
428
441
  const drillLayerPairType = PcbViaStackParser.#readByteIfAvailable(
429
442
  view,
430
443
  PcbViaStackParser.#DRILL_LAYER_PAIR_TYPE_OFFSET + offsetShift
@@ -439,12 +452,20 @@ export class PcbViaStackParser {
439
452
  if (tailSignature) {
440
453
  result.tailSignature = tailSignature
441
454
  }
442
- if (positiveTolerance) {
455
+ if (positiveTolerance !== null) {
443
456
  result.positiveTolerance = positiveTolerance
444
457
  }
445
- if (negativeTolerance) {
458
+ if (negativeTolerance !== null) {
446
459
  result.negativeTolerance = negativeTolerance
447
460
  }
461
+ PcbViaStackParser.#assignHoleTolerance(
462
+ result,
463
+ positiveTolerance,
464
+ negativeTolerance
465
+ )
466
+ if (propagationDelayPs !== null) {
467
+ result.propagationDelayPs = propagationDelayPs
468
+ }
448
469
  if (drillLayerPairType) {
449
470
  result.drillLayerPairType = drillLayerPairType
450
471
  }
@@ -494,6 +515,73 @@ export class PcbViaStackParser {
494
515
  return view.getInt32(offset, true) / 10000
495
516
  }
496
517
 
518
+ /**
519
+ * Reads one optional hole tolerance and suppresses unset sentinel values.
520
+ * @param {DataView} view
521
+ * @param {number} offset
522
+ * @returns {number | null}
523
+ */
524
+ static #readHoleToleranceIfAvailable(view, offset) {
525
+ if (!view || offset + 4 > view.byteLength) {
526
+ return null
527
+ }
528
+
529
+ const rawValue = view.getInt32(offset, true)
530
+ if (
531
+ rawValue === 0 ||
532
+ rawValue === PcbViaStackParser.#HOLE_TOLERANCE_UNSET
533
+ ) {
534
+ return null
535
+ }
536
+
537
+ return rawValue / 10000
538
+ }
539
+
540
+ /**
541
+ * Adds grouped semantic hole tolerance fields when tolerances are present.
542
+ * @param {Record<string, unknown>} result
543
+ * @param {number | null} positiveTolerance
544
+ * @param {number | null} negativeTolerance
545
+ */
546
+ static #assignHoleTolerance(result, positiveTolerance, negativeTolerance) {
547
+ const holeTolerance = {}
548
+
549
+ if (positiveTolerance !== null) {
550
+ holeTolerance.positive = positiveTolerance
551
+ }
552
+ if (negativeTolerance !== null) {
553
+ holeTolerance.negative = negativeTolerance
554
+ }
555
+ if (Object.keys(holeTolerance).length) {
556
+ result.holeTolerance = holeTolerance
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Reads one optional via propagation delay stored as seconds.
562
+ * @param {DataView} view
563
+ * @param {number} offset
564
+ * @returns {number | null}
565
+ */
566
+ static #readPropagationDelayIfAvailable(view, offset) {
567
+ if (!view || offset + 4 > view.byteLength) {
568
+ return null
569
+ }
570
+
571
+ const seconds = view.getFloat32(offset, true)
572
+ const picoseconds = seconds * PcbViaStackParser.#PICOSECONDS_PER_SECOND
573
+
574
+ if (
575
+ !Number.isFinite(picoseconds) ||
576
+ Math.abs(picoseconds) < 0.001 ||
577
+ Math.abs(picoseconds) > 1_000_000
578
+ ) {
579
+ return null
580
+ }
581
+
582
+ return Number(picoseconds.toFixed(4))
583
+ }
584
+
497
585
  /**
498
586
  * Reads one byte when available.
499
587
  * @param {DataView} view
@@ -0,0 +1,335 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { PrintableTextDecoder } from './PrintableTextDecoder.mjs'
6
+
7
+ /**
8
+ * Decodes via-protection sidecar records and links them to via primitives.
9
+ */
10
+ export class PcbViaStructureParser {
11
+ static #STRUCTURE_STREAM_NAMES = [
12
+ 'ViaStructures/Data',
13
+ 'ViaStructureManager/Data'
14
+ ]
15
+
16
+ /**
17
+ * Extracts via-protection structures and primitive links from stream data.
18
+ * @param {Map<string, Uint8Array>} streams
19
+ * @returns {{ structures: object[], links: object[], byPrimitiveIndex: Record<string, object> }}
20
+ */
21
+ static extractFromStreams(streams) {
22
+ const structures = []
23
+ const links = []
24
+
25
+ for (const streamName of PcbViaStructureParser
26
+ .#STRUCTURE_STREAM_NAMES) {
27
+ const records = PcbViaStructureParser.#parseLengthPrefixedRecords(
28
+ streams.get(streamName),
29
+ streamName
30
+ )
31
+
32
+ for (const record of records) {
33
+ const structure =
34
+ PcbViaStructureParser.#parseStructureRecord(record)
35
+ const link = PcbViaStructureParser.#parseLinkRecord(record)
36
+
37
+ if (structure) {
38
+ structures.push(structure)
39
+ }
40
+ if (link) {
41
+ links.push(link)
42
+ }
43
+ }
44
+ }
45
+
46
+ return PcbViaStructureParser.#buildLookup(structures, links)
47
+ }
48
+
49
+ /**
50
+ * Adds via-protection metadata to parsed via primitives in place.
51
+ * @param {object[]} vias
52
+ * @param {{ byPrimitiveIndex?: Record<string, object> }} viaStructures
53
+ */
54
+ static attachToVias(vias, viaStructures) {
55
+ if (!Array.isArray(vias) || !viaStructures?.byPrimitiveIndex) {
56
+ return
57
+ }
58
+
59
+ for (let index = 0; index < vias.length; index += 1) {
60
+ const viaProtection = viaStructures.byPrimitiveIndex[String(index)]
61
+ if (!viaProtection) {
62
+ continue
63
+ }
64
+
65
+ vias[index].viaStructureIndex = viaProtection.viaStructureIndex
66
+ if (viaProtection.ipc4761Type !== undefined) {
67
+ vias[index].ipc4761Type = viaProtection.ipc4761Type
68
+ }
69
+ vias[index].viaProtection = {
70
+ ipc4761Type: viaProtection.ipc4761Type,
71
+ structureType: viaProtection.structureType,
72
+ features: viaProtection.features
73
+ }
74
+ vias[index].drill = {
75
+ holeKind: 'via',
76
+ plating:
77
+ vias[index].isPlated === false ? 'non-plated' : 'plated',
78
+ renderState:
79
+ PcbViaStructureParser.#resolveDrillRenderState(
80
+ viaProtection
81
+ ),
82
+ ipc4761Type: viaProtection.ipc4761Type
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Resolves a display drill state from via-protection metadata.
89
+ * @param {{ ipc4761Type?: number | string, features?: object[] }} viaProtection Via-protection metadata.
90
+ * @returns {'open' | 'covered' | 'filled' | 'capped'}
91
+ */
92
+ static #resolveDrillRenderState(viaProtection) {
93
+ const featureText = (viaProtection?.features || [])
94
+ .flatMap((feature) => [feature.type, feature.material])
95
+ .join(' ')
96
+ .toLowerCase()
97
+
98
+ if (/cap/u.test(featureText)) return 'capped'
99
+ if (/fill|plug/u.test(featureText)) return 'filled'
100
+ if (/cover|tent|mask/u.test(featureText)) return 'covered'
101
+
102
+ const ipcType = Number(viaProtection?.ipc4761Type)
103
+ if (ipcType === 6 || ipcType === 7) return 'capped'
104
+ if (ipcType === 3 || ipcType === 4 || ipcType === 5) return 'filled'
105
+ if (ipcType === 1 || ipcType === 2) return 'covered'
106
+ return 'open'
107
+ }
108
+
109
+ /**
110
+ * Parses one sidecar stream into field records.
111
+ * @param {Uint8Array | undefined} dataBytes
112
+ * @param {string} sourceStream
113
+ * @returns {{ fields: Record<string, string>, sourceStream: string, recordIndex: number }[]}
114
+ */
115
+ static #parseLengthPrefixedRecords(dataBytes, sourceStream) {
116
+ const bytes = PcbViaStructureParser.#toUint8Array(dataBytes)
117
+ const records = []
118
+ let offset = 0
119
+
120
+ while (offset + 4 <= bytes.byteLength) {
121
+ const recordLength = PcbViaStructureParser.#readUint32(
122
+ bytes,
123
+ offset
124
+ )
125
+ offset += 4
126
+
127
+ if (recordLength <= 0 || offset + recordLength > bytes.byteLength) {
128
+ break
129
+ }
130
+
131
+ const recordBytes = bytes.subarray(offset, offset + recordLength)
132
+ offset += recordLength
133
+
134
+ records.push({
135
+ fields: PcbViaStructureParser.#parseRecordFields(recordBytes),
136
+ sourceStream,
137
+ recordIndex: records.length
138
+ })
139
+ }
140
+
141
+ return records
142
+ }
143
+
144
+ /**
145
+ * Parses one pipe-delimited sidecar record.
146
+ * @param {Uint8Array} bytes
147
+ * @returns {Record<string, string>}
148
+ */
149
+ static #parseRecordFields(bytes) {
150
+ const text = PrintableTextDecoder.decodeBytes(bytes)
151
+ .replace(/\u0000/gu, '')
152
+ .trim()
153
+ const fields = {}
154
+
155
+ for (const segment of text.split('|')) {
156
+ const candidate = segment.trim()
157
+ const separatorIndex = candidate.indexOf('=')
158
+ if (separatorIndex <= 0) {
159
+ continue
160
+ }
161
+
162
+ const key = candidate.slice(0, separatorIndex).trim().toUpperCase()
163
+ if (!key) {
164
+ continue
165
+ }
166
+
167
+ fields[key] = candidate.slice(separatorIndex + 1).trim()
168
+ }
169
+
170
+ return fields
171
+ }
172
+
173
+ /**
174
+ * Parses one via structure definition record.
175
+ * @param {{ fields: Record<string, string>, sourceStream: string, recordIndex: number }} record
176
+ * @returns {object | null}
177
+ */
178
+ static #parseStructureRecord(record) {
179
+ if (!('STRUCTURETYPE' in record.fields)) {
180
+ return null
181
+ }
182
+
183
+ const index =
184
+ PcbViaStructureParser.#parseInteger(
185
+ record.fields.VIASTRUCTUREINDEX
186
+ ) ?? record.recordIndex
187
+ const structureType = PcbViaStructureParser.#parseIntegerOrString(
188
+ record.fields.STRUCTURETYPE
189
+ )
190
+
191
+ return {
192
+ index,
193
+ ipc4761Type: structureType,
194
+ structureType,
195
+ sourceStream: record.sourceStream,
196
+ features: PcbViaStructureParser.#parseFeatures(record.fields)
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Parses one primitive-to-structure link record.
202
+ * @param {{ fields: Record<string, string>, sourceStream: string }} record
203
+ * @returns {{ primitiveIndex: number, viaStructureIndex: number, sourceStream: string } | null}
204
+ */
205
+ static #parseLinkRecord(record) {
206
+ const primitiveIndex = PcbViaStructureParser.#parseInteger(
207
+ record.fields.PRIMITIVEINDEX ?? record.fields.VIAINDEX
208
+ )
209
+ const viaStructureIndex = PcbViaStructureParser.#parseInteger(
210
+ record.fields.VIASTRUCTUREINDEX ?? record.fields.STRUCTUREINDEX
211
+ )
212
+
213
+ if (primitiveIndex === null || viaStructureIndex === null) {
214
+ return null
215
+ }
216
+
217
+ return {
218
+ primitiveIndex,
219
+ viaStructureIndex,
220
+ sourceStream: record.sourceStream
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Parses repeated via-protection feature fields.
226
+ * @param {Record<string, string>} fields
227
+ * @returns {{ index: number, type: string, side: string, material: string }[]}
228
+ */
229
+ static #parseFeatures(fields) {
230
+ const features = []
231
+
232
+ for (let index = 0; index < 16; index += 1) {
233
+ const type = fields[`FEATURETYPE${index}`]
234
+ const side = fields[`FEATURESIDE${index}`]
235
+ const material = fields[`FEATUREMATERIAL${index}`]
236
+
237
+ if (!type && !side && !material) {
238
+ continue
239
+ }
240
+
241
+ features.push({
242
+ index,
243
+ type: type || '',
244
+ side: side || '',
245
+ material: material || ''
246
+ })
247
+ }
248
+
249
+ return features
250
+ }
251
+
252
+ /**
253
+ * Builds link lookups keyed by via primitive index.
254
+ * @param {object[]} structures
255
+ * @param {{ primitiveIndex: number, viaStructureIndex: number }[]} links
256
+ * @returns {{ structures: object[], links: object[], byPrimitiveIndex: Record<string, object> }}
257
+ */
258
+ static #buildLookup(structures, links) {
259
+ const structuresByIndex = new Map(
260
+ structures.map((structure) => [structure.index, structure])
261
+ )
262
+ const byPrimitiveIndex = {}
263
+
264
+ for (const link of links) {
265
+ const structure = structuresByIndex.get(link.viaStructureIndex)
266
+ if (!structure) {
267
+ continue
268
+ }
269
+
270
+ byPrimitiveIndex[String(link.primitiveIndex)] = {
271
+ viaStructureIndex: link.viaStructureIndex,
272
+ ipc4761Type: structure.ipc4761Type,
273
+ structureType: structure.structureType,
274
+ features: structure.features
275
+ }
276
+ }
277
+
278
+ return {
279
+ structures,
280
+ links,
281
+ byPrimitiveIndex
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Parses a finite integer from a field value.
287
+ * @param {string | undefined} value
288
+ * @returns {number | null}
289
+ */
290
+ static #parseInteger(value) {
291
+ const number = Number(value)
292
+ return Number.isInteger(number) ? number : null
293
+ }
294
+
295
+ /**
296
+ * Parses a number when possible and otherwise preserves the string value.
297
+ * @param {string | undefined} value
298
+ * @returns {number | string}
299
+ */
300
+ static #parseIntegerOrString(value) {
301
+ const parsed = PcbViaStructureParser.#parseInteger(value)
302
+ return parsed === null ? String(value || '') : parsed
303
+ }
304
+
305
+ /**
306
+ * Reads one little-endian unsigned integer from a byte view.
307
+ * @param {Uint8Array} bytes
308
+ * @param {number} offset
309
+ * @returns {number}
310
+ */
311
+ static #readUint32(bytes, offset) {
312
+ return new DataView(
313
+ bytes.buffer,
314
+ bytes.byteOffset + offset,
315
+ 4
316
+ ).getUint32(0, true)
317
+ }
318
+
319
+ /**
320
+ * Normalizes one byte-like input into a Uint8Array view.
321
+ * @param {Uint8Array | ArrayBuffer | undefined} bytes
322
+ * @returns {Uint8Array}
323
+ */
324
+ static #toUint8Array(bytes) {
325
+ if (!bytes) {
326
+ return new Uint8Array(0)
327
+ }
328
+
329
+ if (bytes instanceof Uint8Array) {
330
+ return bytes
331
+ }
332
+
333
+ return new Uint8Array(bytes)
334
+ }
335
+ }
@@ -6,6 +6,12 @@
6
6
  * Extracts long printable runs from binary Altium documents.
7
7
  */
8
8
  export class PrintableTextDecoder {
9
+ static #WINDOWS_1252_PRINTABLE_CONTROL_BYTES = new Set([
10
+ 0x80, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c,
11
+ 0x8e, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b,
12
+ 0x9c, 0x9e, 0x9f
13
+ ])
14
+
9
15
  /**
10
16
  * Returns printable ASCII-like runs from a binary buffer.
11
17
  * @param {ArrayBuffer} arrayBuffer
@@ -67,8 +73,8 @@ export class PrintableTextDecoder {
67
73
  }
68
74
 
69
75
  /**
70
- * Decodes one byte slice using UTF-8 first, then GB18030 for non-UTF-8
71
- * payloads such as legacy PCB library text.
76
+ * Decodes one byte slice using UTF-8 first, then Windows-1252 or GB18030
77
+ * for non-UTF-8 payloads such as legacy PCB library text.
72
78
  * @param {Uint8Array} bytes
73
79
  * @param {{ encoding?: string }} [options]
74
80
  * @returns {string}
@@ -82,10 +88,34 @@ export class PrintableTextDecoder {
82
88
  new TextDecoder('utf-8').decode(bytes)
83
89
  )
84
90
  }
91
+ if (
92
+ preferredEncoding === 'windows-1252' ||
93
+ preferredEncoding === 'cp1252'
94
+ ) {
95
+ return (
96
+ PrintableTextDecoder.#tryDecode(bytes, 'windows-1252') ||
97
+ new TextDecoder('utf-8').decode(bytes)
98
+ )
99
+ }
100
+
101
+ const utf8 = PrintableTextDecoder.#tryDecode(bytes, 'utf-8')
102
+ if (utf8 !== null) {
103
+ return utf8
104
+ }
105
+
106
+ if (PrintableTextDecoder.#hasWindows1252PreferredBytes(bytes)) {
107
+ const windows1252 = PrintableTextDecoder.#tryDecode(
108
+ bytes,
109
+ 'windows-1252'
110
+ )
111
+ if (windows1252 !== null) {
112
+ return windows1252
113
+ }
114
+ }
85
115
 
86
116
  return (
87
- PrintableTextDecoder.#tryDecode(bytes, 'utf-8') ||
88
117
  PrintableTextDecoder.#tryDecode(bytes, 'gb18030') ||
118
+ PrintableTextDecoder.#tryDecode(bytes, 'windows-1252') ||
89
119
  new TextDecoder('utf-8').decode(bytes)
90
120
  )
91
121
  }
@@ -140,6 +170,26 @@ export class PrintableTextDecoder {
140
170
  .trim()
141
171
  }
142
172
 
173
+ /**
174
+ * Returns true when bytes contain printable Windows-1252 control-range
175
+ * punctuation that can otherwise be misread as GB18030 pairs.
176
+ * @param {Uint8Array} bytes
177
+ * @returns {boolean}
178
+ */
179
+ static #hasWindows1252PreferredBytes(bytes) {
180
+ for (const byte of bytes) {
181
+ if (
182
+ PrintableTextDecoder.#WINDOWS_1252_PRINTABLE_CONTROL_BYTES.has(
183
+ byte
184
+ )
185
+ ) {
186
+ return true
187
+ }
188
+ }
189
+
190
+ return false
191
+ }
192
+
143
193
  /**
144
194
  * Tries one strict decode and returns null when bytes are invalid for it.
145
195
  * @param {Uint8Array} bytes