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