dicom-synth 1.18.0 → 1.19.0
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 +1 -1
- package/dist/esm/collection/generate.js +977 -0
- package/dist/esm/collection/writer.js +243 -109
- package/dist/esm/describe/describe.js +142 -5
- package/dist/esm/index.js +396 -223
- package/dist/esm/nonStandardDicom/dicomdir.js +14 -15
- package/dist/esm/platform/bytes.js +39 -0
- package/dist/esm/platform/random.js +22 -0
- package/dist/esm/platform/sha256.js +131 -0
- package/dist/esm/schema/parametric.js +73 -4
- package/dist/esm/syntheticFixtures/generator.js +31 -14
- package/dist/esm/syntheticFixtures/streamWrite.js +156 -13
- package/dist/esm/syntheticFixtures/uid.js +148 -4
- package/dist/esm/syntheticFixtures/violations.js +18 -6
- package/dist/types/collection/generate.d.ts +43 -0
- package/dist/types/collection/writer.d.ts +0 -27
- package/dist/types/index.d.ts +2 -1
- package/dist/types/nonStandardDicom/dicomdir.d.ts +1 -6
- package/dist/types/platform/bytes.d.ts +4 -0
- package/dist/types/platform/random.d.ts +2 -0
- package/dist/types/platform/sha256.d.ts +1 -0
- package/dist/types/syntheticFixtures/generator.d.ts +2 -2
- package/dist/types/syntheticFixtures/violations.d.ts +1 -1
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -1,12 +1,46 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// src/platform/bytes.ts
|
|
2
|
+
function bytesToHex(bytes) {
|
|
3
|
+
let hex = "";
|
|
4
|
+
for (const byte of bytes) {
|
|
5
|
+
hex += byte.toString(16).padStart(2, "0");
|
|
6
|
+
}
|
|
7
|
+
return hex;
|
|
8
|
+
}
|
|
9
|
+
function utf8Bytes(text) {
|
|
10
|
+
return new TextEncoder().encode(text);
|
|
11
|
+
}
|
|
12
|
+
function concatBytes(...parts) {
|
|
13
|
+
const out = new Uint8Array(parts.reduce((sum, p) => sum + p.length, 0));
|
|
14
|
+
let offset = 0;
|
|
15
|
+
for (const part of parts) {
|
|
16
|
+
out.set(part, offset);
|
|
17
|
+
offset += part.length;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
function patternBytes(length, pattern) {
|
|
22
|
+
const out = new Uint8Array(length);
|
|
23
|
+
const seed = utf8Bytes(pattern);
|
|
24
|
+
if (length === 0 || seed.length === 0) return out;
|
|
25
|
+
out.set(seed.subarray(0, Math.min(seed.length, length)));
|
|
26
|
+
let filled = Math.min(seed.length, length);
|
|
27
|
+
while (filled < length) {
|
|
28
|
+
const chunk = Math.min(filled, length - filled);
|
|
29
|
+
out.copyWithin(filled, 0, chunk);
|
|
30
|
+
filled += chunk;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
6
34
|
|
|
7
|
-
// src/
|
|
8
|
-
|
|
9
|
-
|
|
35
|
+
// src/platform/random.ts
|
|
36
|
+
function randomUint32() {
|
|
37
|
+
return globalThis.crypto.getRandomValues(new Uint32Array(1))[0];
|
|
38
|
+
}
|
|
39
|
+
function randomHex(byteLength) {
|
|
40
|
+
return bytesToHex(
|
|
41
|
+
globalThis.crypto.getRandomValues(new Uint8Array(byteLength))
|
|
42
|
+
);
|
|
43
|
+
}
|
|
10
44
|
|
|
11
45
|
// src/syntheticFixtures/tagTemplate.ts
|
|
12
46
|
var TEMPLATE_VOCAB = [
|
|
@@ -52,6 +86,10 @@ function resolveString(value, context) {
|
|
|
52
86
|
});
|
|
53
87
|
}
|
|
54
88
|
|
|
89
|
+
// src/schema/limits.ts
|
|
90
|
+
var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
|
|
91
|
+
var MAX_STREAM_BYTES = 50 * 1024 * 1024 * 1024;
|
|
92
|
+
|
|
55
93
|
// src/schema/validate.ts
|
|
56
94
|
var TYPE_CAPABILITIES = {
|
|
57
95
|
"valid-image": { image: true },
|
|
@@ -840,23 +878,22 @@ function loadDcmjs() {
|
|
|
840
878
|
var dcmjs = loadDcmjs();
|
|
841
879
|
|
|
842
880
|
// src/nonStandardDicom/dicomdir.ts
|
|
843
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
844
|
-
import { resolve } from "node:path";
|
|
845
881
|
var DICOMDIR_SOP_CLASS_UID = "1.2.840.10008.1.3.10";
|
|
846
882
|
function buildDicomdirBuffer() {
|
|
847
883
|
const uid = DICOMDIR_SOP_CLASS_UID;
|
|
848
|
-
const buf =
|
|
849
|
-
|
|
884
|
+
const buf = new Uint8Array(256);
|
|
885
|
+
const view = new DataView(buf.buffer);
|
|
886
|
+
buf.set(utf8Bytes("DICM"), 128);
|
|
850
887
|
let off = 132;
|
|
851
|
-
|
|
888
|
+
view.setUint16(off, 2, true);
|
|
852
889
|
off += 2;
|
|
853
|
-
|
|
890
|
+
view.setUint16(off, 2, true);
|
|
854
891
|
off += 2;
|
|
855
|
-
buf.
|
|
892
|
+
buf.set(utf8Bytes("UI"), off);
|
|
856
893
|
off += 2;
|
|
857
|
-
|
|
894
|
+
view.setUint16(off, uid.length, true);
|
|
858
895
|
off += 2;
|
|
859
|
-
buf.
|
|
896
|
+
buf.set(utf8Bytes(uid), off);
|
|
860
897
|
off += uid.length;
|
|
861
898
|
return buf.subarray(0, off);
|
|
862
899
|
}
|
|
@@ -1003,7 +1040,7 @@ function serializeDict(meta, dataset, rawElements) {
|
|
|
1003
1040
|
if (rawElements) {
|
|
1004
1041
|
Object.assign(dicomDict.dict, rawElements);
|
|
1005
1042
|
}
|
|
1006
|
-
return
|
|
1043
|
+
return new Uint8Array(dicomDict.write({ allowInvalidVRLength: true }));
|
|
1007
1044
|
}
|
|
1008
1045
|
var MAX_DIMENSION = 65535;
|
|
1009
1046
|
function applySizing(dataset, meta, spec, rawElements) {
|
|
@@ -1048,8 +1085,8 @@ function buildImageBuffer(spec, uid) {
|
|
|
1048
1085
|
return { buffer: serializeDict(meta, dataset, rawElements), rawElements };
|
|
1049
1086
|
}
|
|
1050
1087
|
function buildFakeSignatureBuffer() {
|
|
1051
|
-
const buf =
|
|
1052
|
-
buf.
|
|
1088
|
+
const buf = new Uint8Array(200);
|
|
1089
|
+
buf.set(utf8Bytes("XXXX"), 128);
|
|
1053
1090
|
return buf;
|
|
1054
1091
|
}
|
|
1055
1092
|
function sizedNonDicomBytes(spec) {
|
|
@@ -1057,8 +1094,8 @@ function sizedNonDicomBytes(spec) {
|
|
|
1057
1094
|
}
|
|
1058
1095
|
function buildNonDicomBuffer(spec) {
|
|
1059
1096
|
const bytes = sizedNonDicomBytes(spec);
|
|
1060
|
-
if (bytes !== void 0) return
|
|
1061
|
-
return
|
|
1097
|
+
if (bytes !== void 0) return patternBytes(bytes, "not dicom ");
|
|
1098
|
+
return utf8Bytes(spec.content ?? "not dicom");
|
|
1062
1099
|
}
|
|
1063
1100
|
function buildBufferForSpec(spec, uid) {
|
|
1064
1101
|
switch (spec.type) {
|
|
@@ -1079,19 +1116,143 @@ function buildBufferForSpec(spec, uid) {
|
|
|
1079
1116
|
}
|
|
1080
1117
|
}
|
|
1081
1118
|
|
|
1082
|
-
// src/
|
|
1083
|
-
|
|
1084
|
-
|
|
1119
|
+
// src/platform/sha256.ts
|
|
1120
|
+
var K = new Uint32Array([
|
|
1121
|
+
1116352408,
|
|
1122
|
+
1899447441,
|
|
1123
|
+
3049323471,
|
|
1124
|
+
3921009573,
|
|
1125
|
+
961987163,
|
|
1126
|
+
1508970993,
|
|
1127
|
+
2453635748,
|
|
1128
|
+
2870763221,
|
|
1129
|
+
3624381080,
|
|
1130
|
+
310598401,
|
|
1131
|
+
607225278,
|
|
1132
|
+
1426881987,
|
|
1133
|
+
1925078388,
|
|
1134
|
+
2162078206,
|
|
1135
|
+
2614888103,
|
|
1136
|
+
3248222580,
|
|
1137
|
+
3835390401,
|
|
1138
|
+
4022224774,
|
|
1139
|
+
264347078,
|
|
1140
|
+
604807628,
|
|
1141
|
+
770255983,
|
|
1142
|
+
1249150122,
|
|
1143
|
+
1555081692,
|
|
1144
|
+
1996064986,
|
|
1145
|
+
2554220882,
|
|
1146
|
+
2821834349,
|
|
1147
|
+
2952996808,
|
|
1148
|
+
3210313671,
|
|
1149
|
+
3336571891,
|
|
1150
|
+
3584528711,
|
|
1151
|
+
113926993,
|
|
1152
|
+
338241895,
|
|
1153
|
+
666307205,
|
|
1154
|
+
773529912,
|
|
1155
|
+
1294757372,
|
|
1156
|
+
1396182291,
|
|
1157
|
+
1695183700,
|
|
1158
|
+
1986661051,
|
|
1159
|
+
2177026350,
|
|
1160
|
+
2456956037,
|
|
1161
|
+
2730485921,
|
|
1162
|
+
2820302411,
|
|
1163
|
+
3259730800,
|
|
1164
|
+
3345764771,
|
|
1165
|
+
3516065817,
|
|
1166
|
+
3600352804,
|
|
1167
|
+
4094571909,
|
|
1168
|
+
275423344,
|
|
1169
|
+
430227734,
|
|
1170
|
+
506948616,
|
|
1171
|
+
659060556,
|
|
1172
|
+
883997877,
|
|
1173
|
+
958139571,
|
|
1174
|
+
1322822218,
|
|
1175
|
+
1537002063,
|
|
1176
|
+
1747873779,
|
|
1177
|
+
1955562222,
|
|
1178
|
+
2024104815,
|
|
1179
|
+
2227730452,
|
|
1180
|
+
2361852424,
|
|
1181
|
+
2428436474,
|
|
1182
|
+
2756734187,
|
|
1183
|
+
3204031479,
|
|
1184
|
+
3329325298
|
|
1185
|
+
]);
|
|
1186
|
+
var rotr = (x, n) => x >>> n | x << 32 - n;
|
|
1187
|
+
function sha256(input) {
|
|
1188
|
+
const data2 = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
1189
|
+
const padded = new Uint8Array((data2.length + 8 >> 6) + 1 << 6);
|
|
1190
|
+
padded.set(data2);
|
|
1191
|
+
padded[data2.length] = 128;
|
|
1192
|
+
new DataView(padded.buffer).setBigUint64(
|
|
1193
|
+
padded.length - 8,
|
|
1194
|
+
BigInt(data2.length * 8),
|
|
1195
|
+
false
|
|
1196
|
+
);
|
|
1197
|
+
const h = new Uint32Array([
|
|
1198
|
+
1779033703,
|
|
1199
|
+
3144134277,
|
|
1200
|
+
1013904242,
|
|
1201
|
+
2773480762,
|
|
1202
|
+
1359893119,
|
|
1203
|
+
2600822924,
|
|
1204
|
+
528734635,
|
|
1205
|
+
1541459225
|
|
1206
|
+
]);
|
|
1207
|
+
const w = new Uint32Array(64);
|
|
1208
|
+
const view = new DataView(padded.buffer);
|
|
1209
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
1210
|
+
for (let t = 0; t < 16; t++) w[t] = view.getUint32(offset + t * 4, false);
|
|
1211
|
+
for (let t = 16; t < 64; t++) {
|
|
1212
|
+
const s0 = rotr(w[t - 15], 7) ^ rotr(w[t - 15], 18) ^ w[t - 15] >>> 3;
|
|
1213
|
+
const s1 = rotr(w[t - 2], 17) ^ rotr(w[t - 2], 19) ^ w[t - 2] >>> 10;
|
|
1214
|
+
w[t] = w[t - 16] + s0 + w[t - 7] + s1 >>> 0;
|
|
1215
|
+
}
|
|
1216
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
1217
|
+
for (let t = 0; t < 64; t++) {
|
|
1218
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
1219
|
+
const ch = e & f ^ ~e & g;
|
|
1220
|
+
const temp1 = hh + S1 + ch + K[t] + w[t] >>> 0;
|
|
1221
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
1222
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
1223
|
+
const temp2 = S0 + maj >>> 0;
|
|
1224
|
+
hh = g;
|
|
1225
|
+
g = f;
|
|
1226
|
+
f = e;
|
|
1227
|
+
e = d + temp1 >>> 0;
|
|
1228
|
+
d = c;
|
|
1229
|
+
c = b;
|
|
1230
|
+
b = a;
|
|
1231
|
+
a = temp1 + temp2 >>> 0;
|
|
1232
|
+
}
|
|
1233
|
+
h[0] = h[0] + a >>> 0;
|
|
1234
|
+
h[1] = h[1] + b >>> 0;
|
|
1235
|
+
h[2] = h[2] + c >>> 0;
|
|
1236
|
+
h[3] = h[3] + d >>> 0;
|
|
1237
|
+
h[4] = h[4] + e >>> 0;
|
|
1238
|
+
h[5] = h[5] + f >>> 0;
|
|
1239
|
+
h[6] = h[6] + g >>> 0;
|
|
1240
|
+
h[7] = h[7] + hh >>> 0;
|
|
1241
|
+
}
|
|
1242
|
+
const out = new Uint8Array(32);
|
|
1243
|
+
const outView = new DataView(out.buffer);
|
|
1244
|
+
for (let i = 0; i < 8; i++) outView.setUint32(i * 4, h[i], false);
|
|
1245
|
+
return out;
|
|
1246
|
+
}
|
|
1085
1247
|
|
|
1086
1248
|
// src/syntheticFixtures/uid.ts
|
|
1087
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
1088
1249
|
function hashUid(salt, key) {
|
|
1089
|
-
const digest =
|
|
1090
|
-
const n = BigInt(`0x${digest.subarray(0, 16)
|
|
1250
|
+
const digest = sha256(`${salt} ${key}`);
|
|
1251
|
+
const n = BigInt(`0x${bytesToHex(digest.subarray(0, 16))}`);
|
|
1091
1252
|
return `2.25.${n.toString()}`;
|
|
1092
1253
|
}
|
|
1093
1254
|
function randomUid() {
|
|
1094
|
-
const n = BigInt(`0x${
|
|
1255
|
+
const n = BigInt(`0x${randomHex(16)}`);
|
|
1095
1256
|
return `2.25.${n.toString()}`;
|
|
1096
1257
|
}
|
|
1097
1258
|
function seedToSalt(seed) {
|
|
@@ -1128,176 +1289,6 @@ function makeUidGenerator(salt) {
|
|
|
1128
1289
|
});
|
|
1129
1290
|
}
|
|
1130
1291
|
|
|
1131
|
-
// src/syntheticFixtures/streamWrite.ts
|
|
1132
|
-
var OW_MAX_BYTES = 4294967294;
|
|
1133
|
-
var FRAGMENT_MAX_BYTES = 4294967294;
|
|
1134
|
-
var DEFAULT_CHUNK_BYTES = 8 * 1024 * 1024;
|
|
1135
|
-
var RLE_TRANSFER_SYNTAX_UID = "1.2.840.10008.1.2.5";
|
|
1136
|
-
var PIXEL_DATA_OW_HEADER = Buffer.from([224, 127, 16, 0, 79, 87]);
|
|
1137
|
-
var PIXEL_DATA_OB_HEADER = Buffer.from([224, 127, 16, 0, 79, 66]);
|
|
1138
|
-
var ITEM_TAG_GROUP = 65534;
|
|
1139
|
-
var ITEM_TAG_ELEMENT = 57344;
|
|
1140
|
-
var SEQUENCE_DELIMITER_ELEMENT = 57565;
|
|
1141
|
-
function itemTag(element, byteLength = 0) {
|
|
1142
|
-
const b = Buffer.alloc(8);
|
|
1143
|
-
b.writeUInt16LE(ITEM_TAG_GROUP, 0);
|
|
1144
|
-
b.writeUInt16LE(element, 2);
|
|
1145
|
-
b.writeUInt32LE(byteLength, 4);
|
|
1146
|
-
return b;
|
|
1147
|
-
}
|
|
1148
|
-
function nativeDimensions(pixelBytes) {
|
|
1149
|
-
const cells = Math.max(1, Math.floor(pixelBytes / 2));
|
|
1150
|
-
const rows = Math.min(65535, Math.max(1, Math.round(Math.sqrt(cells))));
|
|
1151
|
-
const columns = Math.min(65535, Math.max(1, Math.floor(cells / rows)));
|
|
1152
|
-
return { rows, columns, bytes: rows * columns * 2 };
|
|
1153
|
-
}
|
|
1154
|
-
function streamZeros(fd, totalBytes, zeroChunk) {
|
|
1155
|
-
let remaining = totalBytes;
|
|
1156
|
-
while (remaining > 0) {
|
|
1157
|
-
const n = Math.min(zeroChunk.length, remaining);
|
|
1158
|
-
writeSync(fd, zeroChunk, 0, n);
|
|
1159
|
-
remaining -= n;
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
function writeNative(outPath, params, dims, zeroChunk) {
|
|
1163
|
-
const { modality, uid, tags } = params;
|
|
1164
|
-
const meta = buildMeta(uid, modality);
|
|
1165
|
-
const dataset = buildBaseImageDataset(uid, modality);
|
|
1166
|
-
const rawElements = applyTagOverrides(dataset, tags);
|
|
1167
|
-
syncMetaWithDataset(meta, dataset, rawElements);
|
|
1168
|
-
dataset.Rows = dims.rows;
|
|
1169
|
-
dataset.Columns = dims.columns;
|
|
1170
|
-
const minimalPixelBytes = dataset.PixelData.byteLength;
|
|
1171
|
-
const probe = serializeDict(meta, dataset, rawElements);
|
|
1172
|
-
const pixelElement = Buffer.alloc(12);
|
|
1173
|
-
PIXEL_DATA_OW_HEADER.copy(pixelElement);
|
|
1174
|
-
pixelElement.writeUInt32LE(minimalPixelBytes, 8);
|
|
1175
|
-
const owIdx = probe.indexOf(pixelElement);
|
|
1176
|
-
if (owIdx < 0) {
|
|
1177
|
-
throw new Error("failed to locate native PixelData element header");
|
|
1178
|
-
}
|
|
1179
|
-
const dataStart = owIdx + 12;
|
|
1180
|
-
const header = Buffer.from(probe.subarray(0, dataStart));
|
|
1181
|
-
header.writeUInt32LE(dims.bytes, owIdx + 8);
|
|
1182
|
-
const trailing = probe.subarray(dataStart + minimalPixelBytes);
|
|
1183
|
-
const fd = openSync(outPath, "w");
|
|
1184
|
-
try {
|
|
1185
|
-
writeSync(fd, header);
|
|
1186
|
-
streamZeros(fd, dims.bytes, zeroChunk);
|
|
1187
|
-
if (trailing.length > 0) writeSync(fd, trailing);
|
|
1188
|
-
} finally {
|
|
1189
|
-
closeSync(fd);
|
|
1190
|
-
}
|
|
1191
|
-
return header.length + dims.bytes + trailing.length;
|
|
1192
|
-
}
|
|
1193
|
-
function writeEncapsulated(outPath, params, pixelBytes, fragmentBytes, zeroChunk) {
|
|
1194
|
-
const { modality, uid, tags } = params;
|
|
1195
|
-
const meta = {
|
|
1196
|
-
...buildMeta(uid, modality),
|
|
1197
|
-
TransferSyntaxUID: RLE_TRANSFER_SYNTAX_UID
|
|
1198
|
-
};
|
|
1199
|
-
const dataset = buildBaseImageDataset(uid, modality);
|
|
1200
|
-
const rawElements = applyTagOverrides(dataset, tags);
|
|
1201
|
-
syncMetaWithDataset(meta, dataset, rawElements);
|
|
1202
|
-
dataset.PixelData = [new Uint8Array([]).buffer, new Uint8Array([0, 0]).buffer];
|
|
1203
|
-
dataset._vrMap = { PixelData: "OB" };
|
|
1204
|
-
const probe = serializeDict(meta, dataset, rawElements);
|
|
1205
|
-
const idx = probe.indexOf(PIXEL_DATA_OB_HEADER);
|
|
1206
|
-
if (idx < 0 || probe.readUInt32LE(idx + 8) !== 4294967295) {
|
|
1207
|
-
throw new Error("failed to locate encapsulated PixelData element header");
|
|
1208
|
-
}
|
|
1209
|
-
const prefix = Buffer.from(probe.subarray(0, idx + 12));
|
|
1210
|
-
const fd = openSync(outPath, "w");
|
|
1211
|
-
let bytesWritten = 0;
|
|
1212
|
-
try {
|
|
1213
|
-
writeSync(fd, prefix);
|
|
1214
|
-
bytesWritten += prefix.length;
|
|
1215
|
-
const bot = itemTag(ITEM_TAG_ELEMENT, 0);
|
|
1216
|
-
writeSync(fd, bot);
|
|
1217
|
-
bytesWritten += bot.length;
|
|
1218
|
-
let remaining = pixelBytes;
|
|
1219
|
-
while (remaining > 0) {
|
|
1220
|
-
const fragLen = Math.min(fragmentBytes, remaining);
|
|
1221
|
-
const fh = itemTag(ITEM_TAG_ELEMENT, fragLen);
|
|
1222
|
-
writeSync(fd, fh);
|
|
1223
|
-
streamZeros(fd, fragLen, zeroChunk);
|
|
1224
|
-
bytesWritten += fh.length + fragLen;
|
|
1225
|
-
remaining -= fragLen;
|
|
1226
|
-
}
|
|
1227
|
-
const delimiter = itemTag(SEQUENCE_DELIMITER_ELEMENT);
|
|
1228
|
-
writeSync(fd, delimiter);
|
|
1229
|
-
bytesWritten += delimiter.length;
|
|
1230
|
-
} finally {
|
|
1231
|
-
closeSync(fd);
|
|
1232
|
-
}
|
|
1233
|
-
return bytesWritten;
|
|
1234
|
-
}
|
|
1235
|
-
function streamLargeImage(outPath, options) {
|
|
1236
|
-
const {
|
|
1237
|
-
targetBytes,
|
|
1238
|
-
chunkBytes = DEFAULT_CHUNK_BYTES,
|
|
1239
|
-
owMaxBytes = OW_MAX_BYTES,
|
|
1240
|
-
fragmentBytes = FRAGMENT_MAX_BYTES
|
|
1241
|
-
} = options;
|
|
1242
|
-
const modality = options.modality ?? "CT";
|
|
1243
|
-
const uid = options.uid ?? makeUidGenerator(options.uidSalt ?? seedToSalt(options.seed))(0);
|
|
1244
|
-
const identity = { modality, uid, tags: options.tags };
|
|
1245
|
-
mkdirSync2(resolve2(outPath, ".."), { recursive: true });
|
|
1246
|
-
const zeroChunk = Buffer.alloc(Math.max(1, chunkBytes));
|
|
1247
|
-
const probe = serializeDict(
|
|
1248
|
-
buildMeta(uid, modality),
|
|
1249
|
-
buildBaseImageDataset(uid, modality)
|
|
1250
|
-
);
|
|
1251
|
-
const nativeHeaderLen = probe.length - 2;
|
|
1252
|
-
const requestedPixel = Math.max(2, targetBytes - nativeHeaderLen);
|
|
1253
|
-
if (requestedPixel <= owMaxBytes) {
|
|
1254
|
-
const dims = nativeDimensions(requestedPixel);
|
|
1255
|
-
const bytesWritten2 = writeNative(outPath, identity, dims, zeroChunk);
|
|
1256
|
-
return {
|
|
1257
|
-
path: outPath,
|
|
1258
|
-
bytesWritten: bytesWritten2,
|
|
1259
|
-
encoding: "native",
|
|
1260
|
-
pixelBytes: dims.bytes
|
|
1261
|
-
};
|
|
1262
|
-
}
|
|
1263
|
-
const estimateFrags = Math.max(
|
|
1264
|
-
1,
|
|
1265
|
-
Math.ceil((targetBytes - nativeHeaderLen - 16) / fragmentBytes)
|
|
1266
|
-
);
|
|
1267
|
-
const overhead = nativeHeaderLen + 8 + estimateFrags * 8 + 8;
|
|
1268
|
-
let pixelBytes = Math.max(2, targetBytes - overhead);
|
|
1269
|
-
pixelBytes -= pixelBytes % 2;
|
|
1270
|
-
const bytesWritten = writeEncapsulated(
|
|
1271
|
-
outPath,
|
|
1272
|
-
identity,
|
|
1273
|
-
pixelBytes,
|
|
1274
|
-
fragmentBytes - fragmentBytes % 2,
|
|
1275
|
-
zeroChunk
|
|
1276
|
-
);
|
|
1277
|
-
return { path: outPath, bytesWritten, encoding: "encapsulated", pixelBytes };
|
|
1278
|
-
}
|
|
1279
|
-
function writeLargeImageFile(spec, outPath, options = {}) {
|
|
1280
|
-
const { targetBytes } = spec;
|
|
1281
|
-
if (!Number.isInteger(targetBytes)) {
|
|
1282
|
-
throw new Error("writeLargeImageFile: targetBytes must be an integer");
|
|
1283
|
-
}
|
|
1284
|
-
if (targetBytes <= MAX_PIXEL_BYTES) {
|
|
1285
|
-
throw new Error(
|
|
1286
|
-
"writeLargeImageFile is for files above the 512 MB in-memory limit \u2014 use targetSizeKb for smaller files"
|
|
1287
|
-
);
|
|
1288
|
-
}
|
|
1289
|
-
if (targetBytes > MAX_STREAM_BYTES) {
|
|
1290
|
-
throw new Error("writeLargeImageFile: targetBytes exceeds the 50 GB limit");
|
|
1291
|
-
}
|
|
1292
|
-
return streamLargeImage(outPath, {
|
|
1293
|
-
targetBytes,
|
|
1294
|
-
modality: spec.modality,
|
|
1295
|
-
uidSalt: spec.uidSalt,
|
|
1296
|
-
seed: spec.seed,
|
|
1297
|
-
chunkBytes: options.chunkBytes
|
|
1298
|
-
});
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
1292
|
// src/syntheticFixtures/violations.ts
|
|
1302
1293
|
var dcmjsAny = dcmjs;
|
|
1303
1294
|
function parseBuffer(buffer) {
|
|
@@ -1308,7 +1299,7 @@ function parseBuffer(buffer) {
|
|
|
1308
1299
|
return dcmjsAny.data.DicomMessage.readFile(ab);
|
|
1309
1300
|
}
|
|
1310
1301
|
function reserialize(parsed) {
|
|
1311
|
-
return
|
|
1302
|
+
return new Uint8Array(parsed.write({ allowInvalidVRLength: true }));
|
|
1312
1303
|
}
|
|
1313
1304
|
var OVERLONG_UID = `2.25.${"0".repeat(60)}`;
|
|
1314
1305
|
var VIOLATION_LEVEL = {
|
|
@@ -1373,11 +1364,12 @@ function applyByteLevel(buffer, violations) {
|
|
|
1373
1364
|
result = result.subarray(132);
|
|
1374
1365
|
break;
|
|
1375
1366
|
case "malformed-sq-delimiter": {
|
|
1376
|
-
const delimiter =
|
|
1377
|
-
delimiter.
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1367
|
+
const delimiter = new Uint8Array(8);
|
|
1368
|
+
const view = new DataView(delimiter.buffer);
|
|
1369
|
+
view.setUint16(0, 65534, true);
|
|
1370
|
+
view.setUint16(2, 57565, true);
|
|
1371
|
+
view.setUint32(4, 4, true);
|
|
1372
|
+
result = concatBytes(result, delimiter);
|
|
1381
1373
|
break;
|
|
1382
1374
|
}
|
|
1383
1375
|
default:
|
|
@@ -1398,7 +1390,7 @@ function applyViolations(buffer, violations, rawElements) {
|
|
|
1398
1390
|
);
|
|
1399
1391
|
}
|
|
1400
1392
|
|
|
1401
|
-
// src/collection/
|
|
1393
|
+
// src/collection/generate.ts
|
|
1402
1394
|
var NO_EXTENSION_TYPES = /* @__PURE__ */ new Set([
|
|
1403
1395
|
"fake-signature",
|
|
1404
1396
|
"non-dicom"
|
|
@@ -1538,10 +1530,10 @@ function hierarchicalName(studyOrdinal, seriesIndex, instanceNumber) {
|
|
|
1538
1530
|
};
|
|
1539
1531
|
}
|
|
1540
1532
|
function withResolvedSeed(spec) {
|
|
1541
|
-
return spec.seed === void 0 ? { ...spec, seed:
|
|
1533
|
+
return spec.seed === void 0 ? { ...spec, seed: randomUint32() } : spec;
|
|
1542
1534
|
}
|
|
1543
1535
|
function withResolvedSalt(spec) {
|
|
1544
|
-
return spec.uidSalt === void 0 ? { ...spec, uidSalt:
|
|
1536
|
+
return spec.uidSalt === void 0 ? { ...spec, uidSalt: randomHex(16) } : spec;
|
|
1545
1537
|
}
|
|
1546
1538
|
function computeNames(type, index, padWidth, grouped, quirks) {
|
|
1547
1539
|
let names;
|
|
@@ -1740,21 +1732,203 @@ async function* previewCollection(spec) {
|
|
|
1740
1732
|
}
|
|
1741
1733
|
}
|
|
1742
1734
|
}
|
|
1735
|
+
|
|
1736
|
+
// src/collection/writer.ts
|
|
1737
|
+
import { mkdirSync as mkdirSync2 } from "node:fs";
|
|
1738
|
+
import { writeFile } from "node:fs/promises";
|
|
1739
|
+
import { dirname, resolve as resolve2 } from "node:path";
|
|
1740
|
+
|
|
1741
|
+
// src/syntheticFixtures/streamWrite.ts
|
|
1742
|
+
import { closeSync, mkdirSync, openSync, writeSync } from "node:fs";
|
|
1743
|
+
import { resolve } from "node:path";
|
|
1744
|
+
var OW_MAX_BYTES = 4294967294;
|
|
1745
|
+
var FRAGMENT_MAX_BYTES = 4294967294;
|
|
1746
|
+
var DEFAULT_CHUNK_BYTES = 8 * 1024 * 1024;
|
|
1747
|
+
var RLE_TRANSFER_SYNTAX_UID = "1.2.840.10008.1.2.5";
|
|
1748
|
+
var PIXEL_DATA_OW_HEADER = Buffer.from([224, 127, 16, 0, 79, 87]);
|
|
1749
|
+
var PIXEL_DATA_OB_HEADER = Buffer.from([224, 127, 16, 0, 79, 66]);
|
|
1750
|
+
var ITEM_TAG_GROUP = 65534;
|
|
1751
|
+
var ITEM_TAG_ELEMENT = 57344;
|
|
1752
|
+
var SEQUENCE_DELIMITER_ELEMENT = 57565;
|
|
1753
|
+
function asBuffer(bytes) {
|
|
1754
|
+
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1755
|
+
}
|
|
1756
|
+
function itemTag(element, byteLength = 0) {
|
|
1757
|
+
const b = Buffer.alloc(8);
|
|
1758
|
+
b.writeUInt16LE(ITEM_TAG_GROUP, 0);
|
|
1759
|
+
b.writeUInt16LE(element, 2);
|
|
1760
|
+
b.writeUInt32LE(byteLength, 4);
|
|
1761
|
+
return b;
|
|
1762
|
+
}
|
|
1763
|
+
function nativeDimensions(pixelBytes) {
|
|
1764
|
+
const cells = Math.max(1, Math.floor(pixelBytes / 2));
|
|
1765
|
+
const rows = Math.min(65535, Math.max(1, Math.round(Math.sqrt(cells))));
|
|
1766
|
+
const columns = Math.min(65535, Math.max(1, Math.floor(cells / rows)));
|
|
1767
|
+
return { rows, columns, bytes: rows * columns * 2 };
|
|
1768
|
+
}
|
|
1769
|
+
function streamZeros(fd, totalBytes, zeroChunk) {
|
|
1770
|
+
let remaining = totalBytes;
|
|
1771
|
+
while (remaining > 0) {
|
|
1772
|
+
const n = Math.min(zeroChunk.length, remaining);
|
|
1773
|
+
writeSync(fd, zeroChunk, 0, n);
|
|
1774
|
+
remaining -= n;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
function writeNative(outPath, params, dims, zeroChunk) {
|
|
1778
|
+
const { modality, uid, tags } = params;
|
|
1779
|
+
const meta = buildMeta(uid, modality);
|
|
1780
|
+
const dataset = buildBaseImageDataset(uid, modality);
|
|
1781
|
+
const rawElements = applyTagOverrides(dataset, tags);
|
|
1782
|
+
syncMetaWithDataset(meta, dataset, rawElements);
|
|
1783
|
+
dataset.Rows = dims.rows;
|
|
1784
|
+
dataset.Columns = dims.columns;
|
|
1785
|
+
const minimalPixelBytes = dataset.PixelData.byteLength;
|
|
1786
|
+
const probe = asBuffer(serializeDict(meta, dataset, rawElements));
|
|
1787
|
+
const pixelElement = Buffer.alloc(12);
|
|
1788
|
+
PIXEL_DATA_OW_HEADER.copy(pixelElement);
|
|
1789
|
+
pixelElement.writeUInt32LE(minimalPixelBytes, 8);
|
|
1790
|
+
const owIdx = probe.indexOf(pixelElement);
|
|
1791
|
+
if (owIdx < 0) {
|
|
1792
|
+
throw new Error("failed to locate native PixelData element header");
|
|
1793
|
+
}
|
|
1794
|
+
const dataStart = owIdx + 12;
|
|
1795
|
+
const header = Buffer.from(probe.subarray(0, dataStart));
|
|
1796
|
+
header.writeUInt32LE(dims.bytes, owIdx + 8);
|
|
1797
|
+
const trailing = probe.subarray(dataStart + minimalPixelBytes);
|
|
1798
|
+
const fd = openSync(outPath, "w");
|
|
1799
|
+
try {
|
|
1800
|
+
writeSync(fd, header);
|
|
1801
|
+
streamZeros(fd, dims.bytes, zeroChunk);
|
|
1802
|
+
if (trailing.length > 0) writeSync(fd, trailing);
|
|
1803
|
+
} finally {
|
|
1804
|
+
closeSync(fd);
|
|
1805
|
+
}
|
|
1806
|
+
return header.length + dims.bytes + trailing.length;
|
|
1807
|
+
}
|
|
1808
|
+
function writeEncapsulated(outPath, params, pixelBytes, fragmentBytes, zeroChunk) {
|
|
1809
|
+
const { modality, uid, tags } = params;
|
|
1810
|
+
const meta = {
|
|
1811
|
+
...buildMeta(uid, modality),
|
|
1812
|
+
TransferSyntaxUID: RLE_TRANSFER_SYNTAX_UID
|
|
1813
|
+
};
|
|
1814
|
+
const dataset = buildBaseImageDataset(uid, modality);
|
|
1815
|
+
const rawElements = applyTagOverrides(dataset, tags);
|
|
1816
|
+
syncMetaWithDataset(meta, dataset, rawElements);
|
|
1817
|
+
dataset.PixelData = [new Uint8Array([]).buffer, new Uint8Array([0, 0]).buffer];
|
|
1818
|
+
dataset._vrMap = { PixelData: "OB" };
|
|
1819
|
+
const probe = asBuffer(serializeDict(meta, dataset, rawElements));
|
|
1820
|
+
const idx = probe.indexOf(PIXEL_DATA_OB_HEADER);
|
|
1821
|
+
if (idx < 0 || probe.readUInt32LE(idx + 8) !== 4294967295) {
|
|
1822
|
+
throw new Error("failed to locate encapsulated PixelData element header");
|
|
1823
|
+
}
|
|
1824
|
+
const prefix = Buffer.from(probe.subarray(0, idx + 12));
|
|
1825
|
+
const fd = openSync(outPath, "w");
|
|
1826
|
+
let bytesWritten = 0;
|
|
1827
|
+
try {
|
|
1828
|
+
writeSync(fd, prefix);
|
|
1829
|
+
bytesWritten += prefix.length;
|
|
1830
|
+
const bot = itemTag(ITEM_TAG_ELEMENT, 0);
|
|
1831
|
+
writeSync(fd, bot);
|
|
1832
|
+
bytesWritten += bot.length;
|
|
1833
|
+
let remaining = pixelBytes;
|
|
1834
|
+
while (remaining > 0) {
|
|
1835
|
+
const fragLen = Math.min(fragmentBytes, remaining);
|
|
1836
|
+
const fh = itemTag(ITEM_TAG_ELEMENT, fragLen);
|
|
1837
|
+
writeSync(fd, fh);
|
|
1838
|
+
streamZeros(fd, fragLen, zeroChunk);
|
|
1839
|
+
bytesWritten += fh.length + fragLen;
|
|
1840
|
+
remaining -= fragLen;
|
|
1841
|
+
}
|
|
1842
|
+
const delimiter = itemTag(SEQUENCE_DELIMITER_ELEMENT);
|
|
1843
|
+
writeSync(fd, delimiter);
|
|
1844
|
+
bytesWritten += delimiter.length;
|
|
1845
|
+
} finally {
|
|
1846
|
+
closeSync(fd);
|
|
1847
|
+
}
|
|
1848
|
+
return bytesWritten;
|
|
1849
|
+
}
|
|
1850
|
+
function streamLargeImage(outPath, options) {
|
|
1851
|
+
const {
|
|
1852
|
+
targetBytes,
|
|
1853
|
+
chunkBytes = DEFAULT_CHUNK_BYTES,
|
|
1854
|
+
owMaxBytes = OW_MAX_BYTES,
|
|
1855
|
+
fragmentBytes = FRAGMENT_MAX_BYTES
|
|
1856
|
+
} = options;
|
|
1857
|
+
const modality = options.modality ?? "CT";
|
|
1858
|
+
const uid = options.uid ?? makeUidGenerator(options.uidSalt ?? seedToSalt(options.seed))(0);
|
|
1859
|
+
const identity = { modality, uid, tags: options.tags };
|
|
1860
|
+
mkdirSync(resolve(outPath, ".."), { recursive: true });
|
|
1861
|
+
const zeroChunk = Buffer.alloc(Math.max(1, chunkBytes));
|
|
1862
|
+
const probe = serializeDict(
|
|
1863
|
+
buildMeta(uid, modality),
|
|
1864
|
+
buildBaseImageDataset(uid, modality)
|
|
1865
|
+
);
|
|
1866
|
+
const nativeHeaderLen = probe.length - 2;
|
|
1867
|
+
const requestedPixel = Math.max(2, targetBytes - nativeHeaderLen);
|
|
1868
|
+
if (requestedPixel <= owMaxBytes) {
|
|
1869
|
+
const dims = nativeDimensions(requestedPixel);
|
|
1870
|
+
const bytesWritten2 = writeNative(outPath, identity, dims, zeroChunk);
|
|
1871
|
+
return {
|
|
1872
|
+
path: outPath,
|
|
1873
|
+
bytesWritten: bytesWritten2,
|
|
1874
|
+
encoding: "native",
|
|
1875
|
+
pixelBytes: dims.bytes
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
const estimateFrags = Math.max(
|
|
1879
|
+
1,
|
|
1880
|
+
Math.ceil((targetBytes - nativeHeaderLen - 16) / fragmentBytes)
|
|
1881
|
+
);
|
|
1882
|
+
const overhead = nativeHeaderLen + 8 + estimateFrags * 8 + 8;
|
|
1883
|
+
let pixelBytes = Math.max(2, targetBytes - overhead);
|
|
1884
|
+
pixelBytes -= pixelBytes % 2;
|
|
1885
|
+
const bytesWritten = writeEncapsulated(
|
|
1886
|
+
outPath,
|
|
1887
|
+
identity,
|
|
1888
|
+
pixelBytes,
|
|
1889
|
+
fragmentBytes - fragmentBytes % 2,
|
|
1890
|
+
zeroChunk
|
|
1891
|
+
);
|
|
1892
|
+
return { path: outPath, bytesWritten, encoding: "encapsulated", pixelBytes };
|
|
1893
|
+
}
|
|
1894
|
+
function writeLargeImageFile(spec, outPath, options = {}) {
|
|
1895
|
+
const { targetBytes } = spec;
|
|
1896
|
+
if (!Number.isInteger(targetBytes)) {
|
|
1897
|
+
throw new Error("writeLargeImageFile: targetBytes must be an integer");
|
|
1898
|
+
}
|
|
1899
|
+
if (targetBytes <= MAX_PIXEL_BYTES) {
|
|
1900
|
+
throw new Error(
|
|
1901
|
+
"writeLargeImageFile is for files above the 512 MB in-memory limit \u2014 use targetSizeKb for smaller files"
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
if (targetBytes > MAX_STREAM_BYTES) {
|
|
1905
|
+
throw new Error("writeLargeImageFile: targetBytes exceeds the 50 GB limit");
|
|
1906
|
+
}
|
|
1907
|
+
return streamLargeImage(outPath, {
|
|
1908
|
+
targetBytes,
|
|
1909
|
+
modality: spec.modality,
|
|
1910
|
+
uidSalt: spec.uidSalt,
|
|
1911
|
+
seed: spec.seed,
|
|
1912
|
+
chunkBytes: options.chunkBytes
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// src/collection/writer.ts
|
|
1743
1917
|
async function writeCollectionFromSpec(spec, outDir) {
|
|
1744
|
-
const root =
|
|
1745
|
-
|
|
1918
|
+
const root = resolve2(outDir);
|
|
1919
|
+
mkdirSync2(root, { recursive: true });
|
|
1746
1920
|
const manifest = [];
|
|
1747
1921
|
const salt = effectiveSalt(spec);
|
|
1748
1922
|
const createdDirs = /* @__PURE__ */ new Set([root]);
|
|
1749
1923
|
const ensureDir = (filePath) => {
|
|
1750
1924
|
const dir = dirname(filePath);
|
|
1751
1925
|
if (!createdDirs.has(dir)) {
|
|
1752
|
-
|
|
1926
|
+
mkdirSync2(dir, { recursive: true });
|
|
1753
1927
|
createdDirs.add(dir);
|
|
1754
1928
|
}
|
|
1755
1929
|
};
|
|
1756
1930
|
for (const plan of planCollection(spec)) {
|
|
1757
|
-
const filePath =
|
|
1931
|
+
const filePath = resolve2(root, plan.relativePath);
|
|
1758
1932
|
ensureDir(filePath);
|
|
1759
1933
|
if (plan.fileSpec.type === "large-image") {
|
|
1760
1934
|
const large = plan.fileSpec;
|
|
@@ -1784,7 +1958,7 @@ async function writeCollectionFromSpec(spec, outDir) {
|
|
|
1784
1958
|
}
|
|
1785
1959
|
|
|
1786
1960
|
// src/describe/describe.ts
|
|
1787
|
-
import { createHash
|
|
1961
|
+
import { createHash } from "node:crypto";
|
|
1788
1962
|
import {
|
|
1789
1963
|
closeSync as closeSync2,
|
|
1790
1964
|
openSync as openSync2,
|
|
@@ -2130,7 +2304,7 @@ function hashUidTags(uids, salt, map) {
|
|
|
2130
2304
|
return tags;
|
|
2131
2305
|
}
|
|
2132
2306
|
function deriveSalt(studyUids) {
|
|
2133
|
-
const h =
|
|
2307
|
+
const h = createHash("sha256");
|
|
2134
2308
|
for (const uid of [...studyUids].sort()) h.update(uid).update("\n");
|
|
2135
2309
|
return h.digest("hex");
|
|
2136
2310
|
}
|
|
@@ -2561,16 +2735,16 @@ function loadCaseById(casesJsonPath, id) {
|
|
|
2561
2735
|
}
|
|
2562
2736
|
|
|
2563
2737
|
// src/public-fixtures/fetch.ts
|
|
2564
|
-
import { createHash as
|
|
2565
|
-
import { existsSync, mkdirSync as
|
|
2738
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2739
|
+
import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync } from "node:fs";
|
|
2566
2740
|
import { homedir } from "node:os";
|
|
2567
2741
|
import { join as join3 } from "node:path";
|
|
2568
2742
|
var DEFAULT_CACHE_ROOT = join3(homedir(), ".cache", "dicom-synth-testcases");
|
|
2569
|
-
function caseCachePath(
|
|
2570
|
-
return join3(root,
|
|
2743
|
+
function caseCachePath(sha2562, root = DEFAULT_CACHE_ROOT) {
|
|
2744
|
+
return join3(root, sha2562, "file.dcm");
|
|
2571
2745
|
}
|
|
2572
2746
|
function verifySha256(buffer, expected) {
|
|
2573
|
-
const h =
|
|
2747
|
+
const h = createHash2("sha256").update(buffer).digest("hex");
|
|
2574
2748
|
if (h !== expected) {
|
|
2575
2749
|
throw new Error(
|
|
2576
2750
|
`SHA256 mismatch: expected ${expected}, got ${h}. Upstream may have changed; bump metadata after review.`
|
|
@@ -2589,7 +2763,7 @@ async function fetchPublicCaseToCache(record, cacheRoot = DEFAULT_CACHE_ROOT) {
|
|
|
2589
2763
|
verifySha256(buf2, record.sha256);
|
|
2590
2764
|
return dest;
|
|
2591
2765
|
}
|
|
2592
|
-
|
|
2766
|
+
mkdirSync3(join3(cacheRoot, record.sha256), { recursive: true });
|
|
2593
2767
|
const res = await fetch(record.source.url);
|
|
2594
2768
|
if (!res.ok) {
|
|
2595
2769
|
throw new Error(
|
|
@@ -2598,12 +2772,11 @@ async function fetchPublicCaseToCache(record, cacheRoot = DEFAULT_CACHE_ROOT) {
|
|
|
2598
2772
|
}
|
|
2599
2773
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
2600
2774
|
verifySha256(buf, record.sha256);
|
|
2601
|
-
|
|
2775
|
+
writeFileSync(dest, buf);
|
|
2602
2776
|
return dest;
|
|
2603
2777
|
}
|
|
2604
2778
|
|
|
2605
2779
|
// src/schema/parametric.ts
|
|
2606
|
-
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
2607
2780
|
function sample(next, range) {
|
|
2608
2781
|
if (typeof range === "number") return range;
|
|
2609
2782
|
return range.min + next() % (range.max - range.min + 1);
|
|
@@ -2622,7 +2795,7 @@ function fraction(next) {
|
|
|
2622
2795
|
}
|
|
2623
2796
|
function resolveParametricSpec(spec) {
|
|
2624
2797
|
const validated = validateParametricSpec(spec);
|
|
2625
|
-
const seed = validated.seed ??
|
|
2798
|
+
const seed = validated.seed ?? randomUint32();
|
|
2626
2799
|
const next = seededStream(seed, PARAMETRIC_STREAM_OFFSET, 0, 0);
|
|
2627
2800
|
const params = validated.studies;
|
|
2628
2801
|
const studies = [];
|