altium-toolkit 1.1.2 → 1.1.3
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/package.json +1 -1
- package/src/core/altium/AltiumLayoutParser.mjs +275 -13
- package/src/core/altium/AltiumParser.mjs +240 -8
- package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
- 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/SchematicImageParser.mjs +291 -6
- package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
- package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
- package/src/core/altium/SchematicPinParser.mjs +175 -4
- package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
- package/src/core/altium/SchematicTextParser.mjs +125 -11
- package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
- package/src/ui/SchematicColorResolver.mjs +78 -0
- package/src/ui/SchematicContentLayout.mjs +58 -1
- package/src/ui/SchematicImageRenderer.mjs +125 -10
- package/src/ui/SchematicJunctionRenderer.mjs +1 -1
- package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
- package/src/ui/SchematicNoteRenderer.mjs +82 -6
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +292 -3
- package/src/ui/SchematicPinSvgRenderer.mjs +197 -15
- 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 +82 -23
- package/src/ui/SchematicSvgRenderer.mjs +293 -47
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { unzlibSync, zlibSync } from 'fflate'
|
|
6
|
+
import { SchematicPowerDiagramLineMasks } from './SchematicPowerDiagramLineMasks.mjs'
|
|
7
|
+
|
|
8
|
+
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]
|
|
9
|
+
const BLUE = [0x00, 0x91, 0xac]
|
|
10
|
+
const ORANGE = [0xa8, 0x4a, 0x12]
|
|
11
|
+
const MAX_CACHE_ENTRIES = 8
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Applies schematic palette hints to recovered bitmap power diagrams.
|
|
15
|
+
*/
|
|
16
|
+
export class SchematicPowerDiagramImageProcessor {
|
|
17
|
+
static #processedCache = new Map()
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Processes one embedded PNG payload and returns a replacement base64 body.
|
|
21
|
+
* Unsupported images are returned unchanged.
|
|
22
|
+
* @param {{ dataBase64?: string, mimeType?: string }} image Embedded image.
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
static process(image) {
|
|
26
|
+
const dataBase64 = String(image?.dataBase64 || '')
|
|
27
|
+
|
|
28
|
+
if (!dataBase64 || image?.mimeType !== 'image/png') {
|
|
29
|
+
return dataBase64
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
SchematicPowerDiagramImageProcessor.#processedCache.has(dataBase64)
|
|
34
|
+
) {
|
|
35
|
+
return SchematicPowerDiagramImageProcessor.#processedCache.get(
|
|
36
|
+
dataBase64
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const png = SchematicPowerDiagramImageProcessor.#decodePngRgba(
|
|
42
|
+
SchematicPowerDiagramImageProcessor.#decodeBase64(dataBase64)
|
|
43
|
+
)
|
|
44
|
+
SchematicPowerDiagramImageProcessor.#recolorPowerDiagram(png)
|
|
45
|
+
const processed = SchematicPowerDiagramImageProcessor.#encodeBase64(
|
|
46
|
+
SchematicPowerDiagramImageProcessor.#encodePngRgba(png)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
SchematicPowerDiagramImageProcessor.#cacheResult(
|
|
50
|
+
dataBase64,
|
|
51
|
+
processed
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return processed
|
|
55
|
+
} catch {
|
|
56
|
+
return dataBase64
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Stores one processed result while bounding memory use.
|
|
62
|
+
* @param {string} source Source base64.
|
|
63
|
+
* @param {string} processed Processed base64.
|
|
64
|
+
*/
|
|
65
|
+
static #cacheResult(source, processed) {
|
|
66
|
+
SchematicPowerDiagramImageProcessor.#processedCache.set(
|
|
67
|
+
source,
|
|
68
|
+
processed
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
while (
|
|
72
|
+
SchematicPowerDiagramImageProcessor.#processedCache.size >
|
|
73
|
+
MAX_CACHE_ENTRIES
|
|
74
|
+
) {
|
|
75
|
+
const oldestKey =
|
|
76
|
+
SchematicPowerDiagramImageProcessor.#processedCache
|
|
77
|
+
.keys()
|
|
78
|
+
.next().value
|
|
79
|
+
SchematicPowerDiagramImageProcessor.#processedCache.delete(
|
|
80
|
+
oldestKey
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Recolors one decoded power-diagram bitmap in place.
|
|
87
|
+
* @param {{ width: number, height: number, rgba: Uint8Array }} png PNG data.
|
|
88
|
+
*/
|
|
89
|
+
static #recolorPowerDiagram(png) {
|
|
90
|
+
const protectedMask =
|
|
91
|
+
SchematicPowerDiagramImageProcessor.#buildProtectedMask(png)
|
|
92
|
+
const darkMask = SchematicPowerDiagramImageProcessor.#buildDarkMask(
|
|
93
|
+
png,
|
|
94
|
+
protectedMask
|
|
95
|
+
)
|
|
96
|
+
const railMask =
|
|
97
|
+
SchematicPowerDiagramImageProcessor.#buildHorizontalRailMask(
|
|
98
|
+
png,
|
|
99
|
+
darkMask
|
|
100
|
+
)
|
|
101
|
+
const orangeMask =
|
|
102
|
+
SchematicPowerDiagramImageProcessor.#buildPowerPortMask(
|
|
103
|
+
png,
|
|
104
|
+
darkMask,
|
|
105
|
+
railMask,
|
|
106
|
+
protectedMask
|
|
107
|
+
)
|
|
108
|
+
const blueMask = SchematicPowerDiagramLineMasks.buildConnectedLineMask(
|
|
109
|
+
png,
|
|
110
|
+
darkMask,
|
|
111
|
+
railMask
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
for (let index = 0; index < darkMask.length; index += 1) {
|
|
115
|
+
if (!darkMask[index]) continue
|
|
116
|
+
|
|
117
|
+
const offset = index * 4
|
|
118
|
+
|
|
119
|
+
if (orangeMask[index]) {
|
|
120
|
+
png.rgba[offset] = ORANGE[0]
|
|
121
|
+
png.rgba[offset + 1] = ORANGE[1]
|
|
122
|
+
png.rgba[offset + 2] = ORANGE[2]
|
|
123
|
+
} else if (blueMask[index]) {
|
|
124
|
+
png.rgba[offset] = BLUE[0]
|
|
125
|
+
png.rgba[offset + 1] = BLUE[1]
|
|
126
|
+
png.rgba[offset + 2] = BLUE[2]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Protects colored block bodies and nearby black text/borders from
|
|
133
|
+
* recoloring.
|
|
134
|
+
* @param {{ width: number, height: number, rgba: Uint8Array }} png PNG data.
|
|
135
|
+
* @returns {Uint8Array}
|
|
136
|
+
*/
|
|
137
|
+
static #buildProtectedMask(png) {
|
|
138
|
+
const colored = new Uint8Array(png.width * png.height)
|
|
139
|
+
const radius = Math.max(
|
|
140
|
+
5,
|
|
141
|
+
Math.round(Math.min(png.width, png.height) * 0.006)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
for (let index = 0; index < colored.length; index += 1) {
|
|
145
|
+
const offset = index * 4
|
|
146
|
+
if (
|
|
147
|
+
png.rgba[offset + 3] > 127 &&
|
|
148
|
+
SchematicPowerDiagramImageProcessor.#isColoredFillPixel(
|
|
149
|
+
png.rgba[offset],
|
|
150
|
+
png.rgba[offset + 1],
|
|
151
|
+
png.rgba[offset + 2]
|
|
152
|
+
)
|
|
153
|
+
) {
|
|
154
|
+
colored[index] = 1
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return SchematicPowerDiagramImageProcessor.#dilateMask(
|
|
159
|
+
colored,
|
|
160
|
+
png.width,
|
|
161
|
+
png.height,
|
|
162
|
+
radius
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Builds a mask of dark source artwork outside protected colored blocks.
|
|
168
|
+
* @param {{ rgba: Uint8Array }} png PNG data.
|
|
169
|
+
* @param {Uint8Array} protectedMask Protected pixels.
|
|
170
|
+
* @returns {Uint8Array}
|
|
171
|
+
*/
|
|
172
|
+
static #buildDarkMask(png, protectedMask) {
|
|
173
|
+
const darkMask = new Uint8Array(protectedMask.length)
|
|
174
|
+
|
|
175
|
+
for (let index = 0; index < darkMask.length; index += 1) {
|
|
176
|
+
if (protectedMask[index]) continue
|
|
177
|
+
|
|
178
|
+
const offset = index * 4
|
|
179
|
+
const red = png.rgba[offset]
|
|
180
|
+
const green = png.rgba[offset + 1]
|
|
181
|
+
const blue = png.rgba[offset + 2]
|
|
182
|
+
|
|
183
|
+
if (png.rgba[offset + 3] > 127 && Math.max(red, green, blue) < 92) {
|
|
184
|
+
darkMask[index] = 1
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return darkMask
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Builds a mask for long horizontal power rails and their junction dots.
|
|
193
|
+
* @param {{ width: number, height: number }} png PNG data.
|
|
194
|
+
* @param {Uint8Array} darkMask Dark source pixels.
|
|
195
|
+
* @returns {Uint8Array}
|
|
196
|
+
*/
|
|
197
|
+
static #buildHorizontalRailMask(png, darkMask) {
|
|
198
|
+
const blueMask = new Uint8Array(darkMask.length)
|
|
199
|
+
const minimumRun = Math.max(24, Math.round(png.width * 0.04))
|
|
200
|
+
const verticalRadius = Math.max(2, Math.round(png.height * 0.003))
|
|
201
|
+
|
|
202
|
+
for (let y = 0; y < png.height; y += 1) {
|
|
203
|
+
let x = 0
|
|
204
|
+
|
|
205
|
+
while (x < png.width) {
|
|
206
|
+
const start = x
|
|
207
|
+
while (x < png.width && darkMask[y * png.width + x]) {
|
|
208
|
+
x += 1
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (x - start >= minimumRun) {
|
|
212
|
+
SchematicPowerDiagramImageProcessor.#markDarkWindow(
|
|
213
|
+
blueMask,
|
|
214
|
+
darkMask,
|
|
215
|
+
png.width,
|
|
216
|
+
png.height,
|
|
217
|
+
start,
|
|
218
|
+
y - verticalRadius,
|
|
219
|
+
x - 1,
|
|
220
|
+
y + verticalRadius
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
x = Math.max(x + 1, start + 1)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return blueMask
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Builds a mask for detected power-port caps, stems, and labels.
|
|
233
|
+
* @param {{ width: number, height: number }} png PNG data.
|
|
234
|
+
* @param {Uint8Array} darkMask Dark source pixels.
|
|
235
|
+
* @param {Uint8Array} blueMask Rail pixels.
|
|
236
|
+
* @param {Uint8Array} protectedMask Protected pixels.
|
|
237
|
+
* @returns {Uint8Array}
|
|
238
|
+
*/
|
|
239
|
+
static #buildPowerPortMask(png, darkMask, blueMask, protectedMask) {
|
|
240
|
+
const orangeMask = new Uint8Array(darkMask.length)
|
|
241
|
+
const minimumCap = Math.max(6, Math.round(png.width * 0.006))
|
|
242
|
+
const maximumCap = Math.max(18, Math.round(png.width * 0.021))
|
|
243
|
+
const maximumStem = Math.max(18, Math.round(png.height * 0.085))
|
|
244
|
+
const stemRadius = Math.max(2, Math.round(png.width * 0.0012))
|
|
245
|
+
const labelDetectionHalfWidth = Math.max(
|
|
246
|
+
14,
|
|
247
|
+
Math.round(png.width * 0.035)
|
|
248
|
+
)
|
|
249
|
+
const labelHalfWidth = Math.max(24, Math.round(png.width * 0.095))
|
|
250
|
+
const labelHeight = Math.max(12, Math.round(png.height * 0.06))
|
|
251
|
+
|
|
252
|
+
for (let y = 0; y < png.height; y += 1) {
|
|
253
|
+
let x = 0
|
|
254
|
+
|
|
255
|
+
while (x < png.width) {
|
|
256
|
+
const start = x
|
|
257
|
+
while (x < png.width && darkMask[y * png.width + x]) {
|
|
258
|
+
x += 1
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const runLength = x - start
|
|
262
|
+
if (runLength >= minimumCap && runLength <= maximumCap) {
|
|
263
|
+
const centerX = Math.round((start + x - 1) / 2)
|
|
264
|
+
const railY =
|
|
265
|
+
SchematicPowerDiagramImageProcessor.#findRailBelow(
|
|
266
|
+
blueMask,
|
|
267
|
+
darkMask,
|
|
268
|
+
png.width,
|
|
269
|
+
png.height,
|
|
270
|
+
centerX,
|
|
271
|
+
y,
|
|
272
|
+
maximumStem,
|
|
273
|
+
stemRadius
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if (railY !== null) {
|
|
277
|
+
if (
|
|
278
|
+
!SchematicPowerDiagramImageProcessor.#hasPowerPortLabelAbove(
|
|
279
|
+
darkMask,
|
|
280
|
+
protectedMask,
|
|
281
|
+
png.width,
|
|
282
|
+
png.height,
|
|
283
|
+
centerX,
|
|
284
|
+
y,
|
|
285
|
+
labelDetectionHalfWidth,
|
|
286
|
+
labelHeight
|
|
287
|
+
)
|
|
288
|
+
) {
|
|
289
|
+
x = Math.max(x + 1, start + 1)
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
SchematicPowerDiagramImageProcessor.#markLabelWindow(
|
|
294
|
+
orangeMask,
|
|
295
|
+
darkMask,
|
|
296
|
+
protectedMask,
|
|
297
|
+
png.width,
|
|
298
|
+
png.height,
|
|
299
|
+
centerX - labelHalfWidth,
|
|
300
|
+
y - labelHeight,
|
|
301
|
+
centerX + labelHalfWidth,
|
|
302
|
+
y - stemRadius
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
x = Math.max(x + 1, start + 1)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
SchematicPowerDiagramLineMasks.removeNarrowLineComponents(
|
|
312
|
+
orangeMask,
|
|
313
|
+
png.width,
|
|
314
|
+
png.height
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return orangeMask
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Returns true when a cap candidate has label-like dark pixels above it.
|
|
322
|
+
* @param {Uint8Array} darkMask Dark source pixels.
|
|
323
|
+
* @param {Uint8Array} protectedMask Protected pixels.
|
|
324
|
+
* @param {number} width Image width.
|
|
325
|
+
* @param {number} height Image height.
|
|
326
|
+
* @param {number} centerX Cap center x.
|
|
327
|
+
* @param {number} y Cap y.
|
|
328
|
+
* @param {number} halfWidth Label search half-width.
|
|
329
|
+
* @param {number} labelHeight Label search height.
|
|
330
|
+
* @returns {boolean}
|
|
331
|
+
*/
|
|
332
|
+
static #hasPowerPortLabelAbove(
|
|
333
|
+
darkMask,
|
|
334
|
+
protectedMask,
|
|
335
|
+
width,
|
|
336
|
+
height,
|
|
337
|
+
centerX,
|
|
338
|
+
y,
|
|
339
|
+
halfWidth,
|
|
340
|
+
labelHeight
|
|
341
|
+
) {
|
|
342
|
+
const left = Math.max(0, Math.floor(centerX - halfWidth))
|
|
343
|
+
const right = Math.min(width - 1, Math.ceil(centerX + halfWidth))
|
|
344
|
+
const top = Math.max(0, Math.floor(y - labelHeight))
|
|
345
|
+
const bottom = Math.min(height - 1, Math.ceil(y - 2))
|
|
346
|
+
const minimumPixels = Math.max(32, Math.round(labelHeight * 1.5))
|
|
347
|
+
const maximumLineSpan = Math.max(6, Math.round((right - left) * 0.85))
|
|
348
|
+
let pixels = 0
|
|
349
|
+
let wideRows = 0
|
|
350
|
+
|
|
351
|
+
for (let row = top; row <= bottom; row += 1) {
|
|
352
|
+
let rowPixels = 0
|
|
353
|
+
let rowLeft = null
|
|
354
|
+
let rowRight = null
|
|
355
|
+
const rowOffset = row * width
|
|
356
|
+
|
|
357
|
+
for (let column = left; column <= right; column += 1) {
|
|
358
|
+
const index = rowOffset + column
|
|
359
|
+
if (!darkMask[index] || protectedMask[index]) {
|
|
360
|
+
continue
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
rowPixels += 1
|
|
364
|
+
rowLeft = rowLeft === null ? column : Math.min(rowLeft, column)
|
|
365
|
+
rowRight =
|
|
366
|
+
rowRight === null ? column : Math.max(rowRight, column)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (rowLeft === null || rowRight === null) continue
|
|
370
|
+
|
|
371
|
+
const rowSpan = rowRight - rowLeft
|
|
372
|
+
if (rowSpan >= maximumLineSpan) continue
|
|
373
|
+
|
|
374
|
+
pixels += rowPixels
|
|
375
|
+
|
|
376
|
+
if (rowSpan >= 5) {
|
|
377
|
+
wideRows += 1
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return pixels >= minimumPixels && wideRows >= 2
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Finds the nearest blue rail below a candidate port cap.
|
|
386
|
+
* @param {Uint8Array} blueMask Rail pixels.
|
|
387
|
+
* @param {Uint8Array} darkMask Dark source pixels.
|
|
388
|
+
* @param {number} width Image width.
|
|
389
|
+
* @param {number} height Image height.
|
|
390
|
+
* @param {number} x Center x.
|
|
391
|
+
* @param {number} y Start y.
|
|
392
|
+
* @param {number} maximumStem Maximum stem length.
|
|
393
|
+
* @param {number} radius Horizontal search radius.
|
|
394
|
+
* @returns {number | null}
|
|
395
|
+
*/
|
|
396
|
+
static #findRailBelow(
|
|
397
|
+
blueMask,
|
|
398
|
+
darkMask,
|
|
399
|
+
width,
|
|
400
|
+
height,
|
|
401
|
+
x,
|
|
402
|
+
y,
|
|
403
|
+
maximumStem,
|
|
404
|
+
radius
|
|
405
|
+
) {
|
|
406
|
+
const endY = Math.min(height - 1, y + maximumStem)
|
|
407
|
+
|
|
408
|
+
for (let candidateY = y + 3; candidateY <= endY; candidateY += 1) {
|
|
409
|
+
if (
|
|
410
|
+
!SchematicPowerDiagramImageProcessor.#windowHasMaskPixel(
|
|
411
|
+
blueMask,
|
|
412
|
+
width,
|
|
413
|
+
height,
|
|
414
|
+
x - radius,
|
|
415
|
+
candidateY,
|
|
416
|
+
x + radius,
|
|
417
|
+
candidateY
|
|
418
|
+
)
|
|
419
|
+
) {
|
|
420
|
+
continue
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (
|
|
424
|
+
SchematicPowerDiagramImageProcessor.#verticalDarkCoverage(
|
|
425
|
+
darkMask,
|
|
426
|
+
width,
|
|
427
|
+
height,
|
|
428
|
+
x,
|
|
429
|
+
y,
|
|
430
|
+
candidateY,
|
|
431
|
+
radius
|
|
432
|
+
) >= 0.62
|
|
433
|
+
) {
|
|
434
|
+
return candidateY
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return null
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Computes dark-pixel coverage along one vertical stem candidate.
|
|
443
|
+
* @param {Uint8Array} darkMask Dark source pixels.
|
|
444
|
+
* @param {number} width Image width.
|
|
445
|
+
* @param {number} height Image height.
|
|
446
|
+
* @param {number} x Center x.
|
|
447
|
+
* @param {number} y1 Start y.
|
|
448
|
+
* @param {number} y2 End y.
|
|
449
|
+
* @param {number} radius Horizontal radius.
|
|
450
|
+
* @returns {number}
|
|
451
|
+
*/
|
|
452
|
+
static #verticalDarkCoverage(darkMask, width, height, x, y1, y2, radius) {
|
|
453
|
+
let coveredRows = 0
|
|
454
|
+
const totalRows = Math.max(y2 - y1 + 1, 1)
|
|
455
|
+
|
|
456
|
+
for (let y = Math.max(0, y1); y <= Math.min(height - 1, y2); y += 1) {
|
|
457
|
+
if (
|
|
458
|
+
SchematicPowerDiagramImageProcessor.#windowHasMaskPixel(
|
|
459
|
+
darkMask,
|
|
460
|
+
width,
|
|
461
|
+
height,
|
|
462
|
+
x - radius,
|
|
463
|
+
y,
|
|
464
|
+
x + radius,
|
|
465
|
+
y
|
|
466
|
+
)
|
|
467
|
+
) {
|
|
468
|
+
coveredRows += 1
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return coveredRows / totalRows
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Marks dark pixels within one rectangle.
|
|
477
|
+
* @param {Uint8Array} target Target mask.
|
|
478
|
+
* @param {Uint8Array} darkMask Dark source mask.
|
|
479
|
+
* @param {number} width Image width.
|
|
480
|
+
* @param {number} height Image height.
|
|
481
|
+
* @param {number} x1 Left.
|
|
482
|
+
* @param {number} y1 Top.
|
|
483
|
+
* @param {number} x2 Right.
|
|
484
|
+
* @param {number} y2 Bottom.
|
|
485
|
+
*/
|
|
486
|
+
static #markDarkWindow(target, darkMask, width, height, x1, y1, x2, y2) {
|
|
487
|
+
const left = Math.max(0, Math.floor(x1))
|
|
488
|
+
const right = Math.min(width - 1, Math.ceil(x2))
|
|
489
|
+
const top = Math.max(0, Math.floor(y1))
|
|
490
|
+
const bottom = Math.min(height - 1, Math.ceil(y2))
|
|
491
|
+
|
|
492
|
+
for (let y = top; y <= bottom; y += 1) {
|
|
493
|
+
const rowOffset = y * width
|
|
494
|
+
for (let x = left; x <= right; x += 1) {
|
|
495
|
+
const index = rowOffset + x
|
|
496
|
+
if (darkMask[index]) {
|
|
497
|
+
target[index] = 1
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Marks dark label pixels while respecting protected colored boxes.
|
|
505
|
+
* @param {Uint8Array} target Target mask.
|
|
506
|
+
* @param {Uint8Array} darkMask Dark source mask.
|
|
507
|
+
* @param {Uint8Array} protectedMask Protected pixels.
|
|
508
|
+
* @param {number} width Image width.
|
|
509
|
+
* @param {number} height Image height.
|
|
510
|
+
* @param {number} x1 Left.
|
|
511
|
+
* @param {number} y1 Top.
|
|
512
|
+
* @param {number} x2 Right.
|
|
513
|
+
* @param {number} y2 Bottom.
|
|
514
|
+
*/
|
|
515
|
+
static #markLabelWindow(
|
|
516
|
+
target,
|
|
517
|
+
darkMask,
|
|
518
|
+
protectedMask,
|
|
519
|
+
width,
|
|
520
|
+
height,
|
|
521
|
+
x1,
|
|
522
|
+
y1,
|
|
523
|
+
x2,
|
|
524
|
+
y2
|
|
525
|
+
) {
|
|
526
|
+
const left = Math.max(0, Math.floor(x1))
|
|
527
|
+
const right = Math.min(width - 1, Math.ceil(x2))
|
|
528
|
+
const top = Math.max(0, Math.floor(y1))
|
|
529
|
+
const bottom = Math.min(height - 1, Math.ceil(y2))
|
|
530
|
+
|
|
531
|
+
for (let y = top; y <= bottom; y += 1) {
|
|
532
|
+
const rowOffset = y * width
|
|
533
|
+
for (let x = left; x <= right; x += 1) {
|
|
534
|
+
const index = rowOffset + x
|
|
535
|
+
if (darkMask[index] && !protectedMask[index]) {
|
|
536
|
+
target[index] = 1
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Returns true when a mask contains any pixel in the given bounds.
|
|
544
|
+
* @param {Uint8Array} mask Pixel mask.
|
|
545
|
+
* @param {number} width Image width.
|
|
546
|
+
* @param {number} height Image height.
|
|
547
|
+
* @param {number} x1 Left.
|
|
548
|
+
* @param {number} y1 Top.
|
|
549
|
+
* @param {number} x2 Right.
|
|
550
|
+
* @param {number} y2 Bottom.
|
|
551
|
+
* @returns {boolean}
|
|
552
|
+
*/
|
|
553
|
+
static #windowHasMaskPixel(mask, width, height, x1, y1, x2, y2) {
|
|
554
|
+
const left = Math.max(0, Math.floor(x1))
|
|
555
|
+
const right = Math.min(width - 1, Math.ceil(x2))
|
|
556
|
+
const top = Math.max(0, Math.floor(y1))
|
|
557
|
+
const bottom = Math.min(height - 1, Math.ceil(y2))
|
|
558
|
+
|
|
559
|
+
for (let y = top; y <= bottom; y += 1) {
|
|
560
|
+
const rowOffset = y * width
|
|
561
|
+
for (let x = left; x <= right; x += 1) {
|
|
562
|
+
if (mask[rowOffset + x]) {
|
|
563
|
+
return true
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return false
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Expands a binary mask using a square window.
|
|
573
|
+
* @param {Uint8Array} mask Input mask.
|
|
574
|
+
* @param {number} width Image width.
|
|
575
|
+
* @param {number} height Image height.
|
|
576
|
+
* @param {number} radius Dilation radius.
|
|
577
|
+
* @returns {Uint8Array}
|
|
578
|
+
*/
|
|
579
|
+
static #dilateMask(mask, width, height, radius) {
|
|
580
|
+
const horizontal = new Uint8Array(mask.length)
|
|
581
|
+
const output = new Uint8Array(mask.length)
|
|
582
|
+
|
|
583
|
+
for (let y = 0; y < height; y += 1) {
|
|
584
|
+
let count = 0
|
|
585
|
+
for (let x = 0; x < width; x += 1) {
|
|
586
|
+
const addX = x + radius
|
|
587
|
+
const removeX = x - radius - 1
|
|
588
|
+
if (addX < width) count += mask[y * width + addX]
|
|
589
|
+
if (removeX >= 0) count -= mask[y * width + removeX]
|
|
590
|
+
if (count > 0) horizontal[y * width + x] = 1
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
for (let x = 0; x < width; x += 1) {
|
|
595
|
+
let count = 0
|
|
596
|
+
for (let y = 0; y < height; y += 1) {
|
|
597
|
+
const addY = y + radius
|
|
598
|
+
const removeY = y - radius - 1
|
|
599
|
+
if (addY < height) count += horizontal[addY * width + x]
|
|
600
|
+
if (removeY >= 0) count -= horizontal[removeY * width + x]
|
|
601
|
+
if (count > 0) output[y * width + x] = 1
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return output
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Returns true for colored block-fill pixels.
|
|
610
|
+
* @param {number} red Red channel.
|
|
611
|
+
* @param {number} green Green channel.
|
|
612
|
+
* @param {number} blue Blue channel.
|
|
613
|
+
* @returns {boolean}
|
|
614
|
+
*/
|
|
615
|
+
static #isColoredFillPixel(red, green, blue) {
|
|
616
|
+
const maximum = Math.max(red, green, blue)
|
|
617
|
+
const minimum = Math.min(red, green, blue)
|
|
618
|
+
|
|
619
|
+
return maximum > 120 && maximum - minimum > 18
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Decodes one non-interlaced 8-bit RGBA PNG.
|
|
624
|
+
* @param {Uint8Array} bytes PNG bytes.
|
|
625
|
+
* @returns {{ width: number, height: number, rgba: Uint8Array }}
|
|
626
|
+
*/
|
|
627
|
+
static #decodePngRgba(bytes) {
|
|
628
|
+
if (
|
|
629
|
+
bytes.length < 33 ||
|
|
630
|
+
!PNG_SIGNATURE.every((byte, index) => bytes[index] === byte)
|
|
631
|
+
) {
|
|
632
|
+
throw new Error('Unsupported PNG signature')
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const chunks = SchematicPowerDiagramImageProcessor.#readPngChunks(bytes)
|
|
636
|
+
const ihdr = chunks.find((chunk) => chunk.type === 'IHDR')?.data
|
|
637
|
+
if (!ihdr) throw new Error('Missing PNG header')
|
|
638
|
+
|
|
639
|
+
const headerView = new DataView(
|
|
640
|
+
ihdr.buffer,
|
|
641
|
+
ihdr.byteOffset,
|
|
642
|
+
ihdr.byteLength
|
|
643
|
+
)
|
|
644
|
+
const width = headerView.getUint32(0, false)
|
|
645
|
+
const height = headerView.getUint32(4, false)
|
|
646
|
+
|
|
647
|
+
if (
|
|
648
|
+
ihdr[8] !== 8 ||
|
|
649
|
+
ihdr[9] !== 6 ||
|
|
650
|
+
ihdr[10] !== 0 ||
|
|
651
|
+
ihdr[11] !== 0 ||
|
|
652
|
+
ihdr[12] !== 0
|
|
653
|
+
) {
|
|
654
|
+
throw new Error('Unsupported PNG encoding')
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const idat = SchematicPowerDiagramImageProcessor.#concatByteArrays(
|
|
658
|
+
chunks
|
|
659
|
+
.filter((chunk) => chunk.type === 'IDAT')
|
|
660
|
+
.map((chunk) => chunk.data)
|
|
661
|
+
)
|
|
662
|
+
const raw = unzlibSync(idat)
|
|
663
|
+
|
|
664
|
+
return {
|
|
665
|
+
width,
|
|
666
|
+
height,
|
|
667
|
+
rgba: SchematicPowerDiagramImageProcessor.#unfilterRgbaScanlines(
|
|
668
|
+
raw,
|
|
669
|
+
width,
|
|
670
|
+
height
|
|
671
|
+
)
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Decodes base64 in both browser and Node runtimes.
|
|
677
|
+
* @param {string} dataBase64 Base64 data.
|
|
678
|
+
* @returns {Uint8Array}
|
|
679
|
+
*/
|
|
680
|
+
static #decodeBase64(dataBase64) {
|
|
681
|
+
if (typeof Buffer !== 'undefined') {
|
|
682
|
+
return Uint8Array.from(Buffer.from(dataBase64, 'base64'))
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const binary = globalThis.atob(dataBase64)
|
|
686
|
+
const bytes = new Uint8Array(binary.length)
|
|
687
|
+
|
|
688
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
689
|
+
bytes[index] = binary.charCodeAt(index)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return bytes
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Encodes base64 in both browser and Node runtimes.
|
|
697
|
+
* @param {Uint8Array} bytes Bytes to encode.
|
|
698
|
+
* @returns {string}
|
|
699
|
+
*/
|
|
700
|
+
static #encodeBase64(bytes) {
|
|
701
|
+
if (typeof Buffer !== 'undefined') {
|
|
702
|
+
return Buffer.from(bytes).toString('base64')
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const chunkSize = 0x8000
|
|
706
|
+
const chunks = []
|
|
707
|
+
|
|
708
|
+
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
|
|
709
|
+
chunks.push(
|
|
710
|
+
String.fromCharCode(
|
|
711
|
+
...bytes.subarray(offset, offset + chunkSize)
|
|
712
|
+
)
|
|
713
|
+
)
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return globalThis.btoa(chunks.join(''))
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Reads PNG chunks without validating checksums.
|
|
721
|
+
* @param {Uint8Array} bytes PNG bytes.
|
|
722
|
+
* @returns {{ type: string, data: Uint8Array }[]}
|
|
723
|
+
*/
|
|
724
|
+
static #readPngChunks(bytes) {
|
|
725
|
+
const chunks = []
|
|
726
|
+
let offset = PNG_SIGNATURE.length
|
|
727
|
+
|
|
728
|
+
while (offset + 12 <= bytes.length) {
|
|
729
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset + offset)
|
|
730
|
+
const length = view.getUint32(0, false)
|
|
731
|
+
const type = String.fromCharCode(
|
|
732
|
+
bytes[offset + 4],
|
|
733
|
+
bytes[offset + 5],
|
|
734
|
+
bytes[offset + 6],
|
|
735
|
+
bytes[offset + 7]
|
|
736
|
+
)
|
|
737
|
+
const dataStart = offset + 8
|
|
738
|
+
const dataEnd = dataStart + length
|
|
739
|
+
|
|
740
|
+
if (dataEnd + 4 > bytes.length) {
|
|
741
|
+
throw new Error('Truncated PNG chunk')
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
chunks.push({
|
|
745
|
+
type,
|
|
746
|
+
data: bytes.slice(dataStart, dataEnd)
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
offset = dataEnd + 4
|
|
750
|
+
if (type === 'IEND') break
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return chunks
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Reconstructs RGBA rows from PNG filtered scanlines.
|
|
758
|
+
* @param {Uint8Array} raw Inflated scanline bytes.
|
|
759
|
+
* @param {number} width Image width.
|
|
760
|
+
* @param {number} height Image height.
|
|
761
|
+
* @returns {Uint8Array}
|
|
762
|
+
*/
|
|
763
|
+
static #unfilterRgbaScanlines(raw, width, height) {
|
|
764
|
+
const bytesPerPixel = 4
|
|
765
|
+
const rowLength = width * bytesPerPixel
|
|
766
|
+
const expectedLength = height * (rowLength + 1)
|
|
767
|
+
const rgba = new Uint8Array(width * height * bytesPerPixel)
|
|
768
|
+
const previous = new Uint8Array(rowLength)
|
|
769
|
+
|
|
770
|
+
if (raw.length < expectedLength) {
|
|
771
|
+
throw new Error('Truncated PNG scanlines')
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
for (let y = 0; y < height; y += 1) {
|
|
775
|
+
const rowStart = y * (rowLength + 1)
|
|
776
|
+
const filter = raw[rowStart]
|
|
777
|
+
const current = rgba.subarray(y * rowLength, (y + 1) * rowLength)
|
|
778
|
+
const source = raw.subarray(rowStart + 1, rowStart + 1 + rowLength)
|
|
779
|
+
|
|
780
|
+
for (let index = 0; index < rowLength; index += 1) {
|
|
781
|
+
const left =
|
|
782
|
+
index >= bytesPerPixel ? current[index - bytesPerPixel] : 0
|
|
783
|
+
const up = previous[index]
|
|
784
|
+
const upLeft =
|
|
785
|
+
index >= bytesPerPixel ? previous[index - bytesPerPixel] : 0
|
|
786
|
+
|
|
787
|
+
current[index] =
|
|
788
|
+
(source[index] +
|
|
789
|
+
SchematicPowerDiagramImageProcessor.#filterPredictor(
|
|
790
|
+
filter,
|
|
791
|
+
left,
|
|
792
|
+
up,
|
|
793
|
+
upLeft
|
|
794
|
+
)) &
|
|
795
|
+
0xff
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
previous.set(current)
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return rgba
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Returns one PNG row-filter predictor.
|
|
806
|
+
* @param {number} filter PNG filter type.
|
|
807
|
+
* @param {number} left Left byte.
|
|
808
|
+
* @param {number} up Previous-row byte.
|
|
809
|
+
* @param {number} upLeft Previous-row left byte.
|
|
810
|
+
* @returns {number}
|
|
811
|
+
*/
|
|
812
|
+
static #filterPredictor(filter, left, up, upLeft) {
|
|
813
|
+
if (filter === 0) return 0
|
|
814
|
+
if (filter === 1) return left
|
|
815
|
+
if (filter === 2) return up
|
|
816
|
+
if (filter === 3) return Math.floor((left + up) / 2)
|
|
817
|
+
if (filter === 4) {
|
|
818
|
+
return SchematicPowerDiagramImageProcessor.#paethPredictor(
|
|
819
|
+
left,
|
|
820
|
+
up,
|
|
821
|
+
upLeft
|
|
822
|
+
)
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
throw new Error('Unsupported PNG row filter')
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Computes the PNG Paeth predictor.
|
|
830
|
+
* @param {number} left Left byte.
|
|
831
|
+
* @param {number} up Previous-row byte.
|
|
832
|
+
* @param {number} upLeft Previous-row left byte.
|
|
833
|
+
* @returns {number}
|
|
834
|
+
*/
|
|
835
|
+
static #paethPredictor(left, up, upLeft) {
|
|
836
|
+
const estimate = left + up - upLeft
|
|
837
|
+
const leftDistance = Math.abs(estimate - left)
|
|
838
|
+
const upDistance = Math.abs(estimate - up)
|
|
839
|
+
const upLeftDistance = Math.abs(estimate - upLeft)
|
|
840
|
+
|
|
841
|
+
if (leftDistance <= upDistance && leftDistance <= upLeftDistance) {
|
|
842
|
+
return left
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return upDistance <= upLeftDistance ? up : upLeft
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Encodes RGBA pixels into a minimal PNG payload.
|
|
850
|
+
* @param {{ width: number, height: number, rgba: Uint8Array }} png PNG data.
|
|
851
|
+
* @returns {Uint8Array}
|
|
852
|
+
*/
|
|
853
|
+
static #encodePngRgba(png) {
|
|
854
|
+
const scanlineLength = png.width * 4 + 1
|
|
855
|
+
const raw = new Uint8Array(scanlineLength * png.height)
|
|
856
|
+
|
|
857
|
+
for (let y = 0; y < png.height; y += 1) {
|
|
858
|
+
const rowOffset = y * scanlineLength
|
|
859
|
+
raw[rowOffset] = 0
|
|
860
|
+
raw.set(
|
|
861
|
+
png.rgba.subarray(y * png.width * 4, (y + 1) * png.width * 4),
|
|
862
|
+
rowOffset + 1
|
|
863
|
+
)
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return SchematicPowerDiagramImageProcessor.#concatByteArrays([
|
|
867
|
+
Uint8Array.from(PNG_SIGNATURE),
|
|
868
|
+
SchematicPowerDiagramImageProcessor.#pngChunk(
|
|
869
|
+
'IHDR',
|
|
870
|
+
SchematicPowerDiagramImageProcessor.#pngHeader(
|
|
871
|
+
png.width,
|
|
872
|
+
png.height
|
|
873
|
+
)
|
|
874
|
+
),
|
|
875
|
+
SchematicPowerDiagramImageProcessor.#pngChunk(
|
|
876
|
+
'IDAT',
|
|
877
|
+
zlibSync(raw, { level: 1 })
|
|
878
|
+
),
|
|
879
|
+
SchematicPowerDiagramImageProcessor.#pngChunk(
|
|
880
|
+
'IEND',
|
|
881
|
+
new Uint8Array()
|
|
882
|
+
)
|
|
883
|
+
])
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Builds PNG IHDR chunk data.
|
|
888
|
+
* @param {number} width Pixel width.
|
|
889
|
+
* @param {number} height Pixel height.
|
|
890
|
+
* @returns {Uint8Array}
|
|
891
|
+
*/
|
|
892
|
+
static #pngHeader(width, height) {
|
|
893
|
+
const header = new Uint8Array(13)
|
|
894
|
+
const view = new DataView(header.buffer)
|
|
895
|
+
|
|
896
|
+
view.setUint32(0, width, false)
|
|
897
|
+
view.setUint32(4, height, false)
|
|
898
|
+
header[8] = 8
|
|
899
|
+
header[9] = 6
|
|
900
|
+
header[10] = 0
|
|
901
|
+
header[11] = 0
|
|
902
|
+
header[12] = 0
|
|
903
|
+
|
|
904
|
+
return header
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Builds a PNG chunk.
|
|
909
|
+
* @param {string} type Four-byte chunk type.
|
|
910
|
+
* @param {Uint8Array} data Chunk data.
|
|
911
|
+
* @returns {Uint8Array}
|
|
912
|
+
*/
|
|
913
|
+
static #pngChunk(type, data) {
|
|
914
|
+
const typeBytes = new TextEncoder().encode(type)
|
|
915
|
+
const chunk = new Uint8Array(12 + data.length)
|
|
916
|
+
const view = new DataView(chunk.buffer)
|
|
917
|
+
|
|
918
|
+
view.setUint32(0, data.length, false)
|
|
919
|
+
chunk.set(typeBytes, 4)
|
|
920
|
+
chunk.set(data, 8)
|
|
921
|
+
view.setUint32(
|
|
922
|
+
8 + data.length,
|
|
923
|
+
SchematicPowerDiagramImageProcessor.#crc32(
|
|
924
|
+
SchematicPowerDiagramImageProcessor.#concatByteArrays([
|
|
925
|
+
typeBytes,
|
|
926
|
+
data
|
|
927
|
+
])
|
|
928
|
+
),
|
|
929
|
+
false
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
return chunk
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Computes a PNG-compatible CRC-32 checksum.
|
|
937
|
+
* @param {Uint8Array} bytes Bytes to checksum.
|
|
938
|
+
* @returns {number}
|
|
939
|
+
*/
|
|
940
|
+
static #crc32(bytes) {
|
|
941
|
+
let crc = 0xffffffff
|
|
942
|
+
|
|
943
|
+
for (const byte of bytes) {
|
|
944
|
+
crc ^= byte
|
|
945
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
946
|
+
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0)
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return (crc ^ 0xffffffff) >>> 0
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Concatenates byte arrays.
|
|
955
|
+
* @param {Uint8Array[]} chunks Byte arrays.
|
|
956
|
+
* @returns {Uint8Array}
|
|
957
|
+
*/
|
|
958
|
+
static #concatByteArrays(chunks) {
|
|
959
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0)
|
|
960
|
+
const output = new Uint8Array(totalLength)
|
|
961
|
+
let offset = 0
|
|
962
|
+
|
|
963
|
+
for (const chunk of chunks) {
|
|
964
|
+
output.set(chunk, offset)
|
|
965
|
+
offset += chunk.length
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
return output
|
|
969
|
+
}
|
|
970
|
+
}
|