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/dist/esm/index.js CHANGED
@@ -1,12 +1,46 @@
1
- // src/collection/writer.ts
2
- import { randomBytes as randomBytes2 } from "node:crypto";
3
- import { mkdirSync as mkdirSync3 } from "node:fs";
4
- import { writeFile } from "node:fs/promises";
5
- import { dirname, resolve as resolve3 } from "node:path";
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/schema/limits.ts
8
- var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
9
- var MAX_STREAM_BYTES = 50 * 1024 * 1024 * 1024;
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 = Buffer.alloc(256, 0);
849
- buf.write("DICM", 128, 4, "ascii");
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
- buf.writeUInt16LE(2, off);
888
+ view.setUint16(off, 2, true);
852
889
  off += 2;
853
- buf.writeUInt16LE(2, off);
890
+ view.setUint16(off, 2, true);
854
891
  off += 2;
855
- buf.write("UI", off, 2, "ascii");
892
+ buf.set(utf8Bytes("UI"), off);
856
893
  off += 2;
857
- buf.writeUInt16LE(uid.length, off);
894
+ view.setUint16(off, uid.length, true);
858
895
  off += 2;
859
- buf.write(uid, off, uid.length, "ascii");
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 Buffer.from(dicomDict.write({ allowInvalidVRLength: true }));
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 = Buffer.alloc(200, 0);
1052
- buf.write("XXXX", 128, 4, "ascii");
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 Buffer.alloc(bytes, "not dicom ");
1061
- return Buffer.from(spec.content ?? "not dicom", "utf8");
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/syntheticFixtures/streamWrite.ts
1083
- import { closeSync, mkdirSync as mkdirSync2, openSync, writeSync } from "node:fs";
1084
- import { resolve as resolve2 } from "node:path";
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 = createHash("sha256").update(salt).update(" ").update(key).digest();
1090
- const n = BigInt(`0x${digest.subarray(0, 16).toString("hex")}`);
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${randomBytes(16).toString("hex")}`);
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 Buffer.from(parsed.write({ allowInvalidVRLength: true }));
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 = Buffer.alloc(8);
1377
- delimiter.writeUInt16LE(65534, 0);
1378
- delimiter.writeUInt16LE(57565, 2);
1379
- delimiter.writeUInt32LE(4, 4);
1380
- result = Buffer.concat([result, delimiter]);
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/writer.ts
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: randomBytes2(4).readUInt32BE(0) } : spec;
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: randomBytes2(16).toString("hex") } : spec;
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 = resolve3(outDir);
1745
- mkdirSync3(root, { recursive: true });
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
- mkdirSync3(dir, { recursive: true });
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 = resolve3(root, plan.relativePath);
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 as createHash2 } from "node:crypto";
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 = createHash2("sha256");
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 createHash3 } from "node:crypto";
2565
- import { existsSync, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
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(sha256, root = DEFAULT_CACHE_ROOT) {
2570
- return join3(root, sha256, "file.dcm");
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 = createHash3("sha256").update(buffer).digest("hex");
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
- mkdirSync4(join3(cacheRoot, record.sha256), { recursive: true });
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
- writeFileSync2(dest, buf);
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 ?? randomBytes3(4).readUInt32BE(0);
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 = [];