dicom-synth 1.14.0 → 1.16.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/bin/dicom-synth-describe.mjs +14 -2
- package/dist/esm/collection/writer.js +167 -45
- package/dist/esm/describe/describe.js +314 -40
- package/dist/esm/index.js +473 -89
- package/dist/esm/schema/parametric.js +44 -2
- package/dist/esm/schema/validate.js +154 -4
- package/dist/esm/syntheticFixtures/generator.js +20 -4
- package/dist/esm/syntheticFixtures/streamWrite.js +10 -0
- package/dist/types/describe/describe.d.ts +2 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/schema/types.d.ts +13 -0
- package/dist/types/schema/validate.d.ts +3 -1
- package/dist/types/syntheticFixtures/generator.d.ts +3 -1
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
readSync,
|
|
9
9
|
statSync
|
|
10
10
|
} from "node:fs";
|
|
11
|
-
import { join } from "node:path";
|
|
11
|
+
import { basename, extname, join, relative, sep } from "node:path";
|
|
12
12
|
|
|
13
13
|
// src/loadDcmjs.ts
|
|
14
14
|
import dcmjsDefaultImport, * as dcmjsNamespace from "dcmjs";
|
|
@@ -113,11 +113,14 @@ function validateUidSalt(value, path) {
|
|
|
113
113
|
);
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
|
-
function
|
|
117
|
-
if (
|
|
116
|
+
function validateInMemoryByteLimit(bytes, path) {
|
|
117
|
+
if (bytes > MAX_PIXEL_BYTES) {
|
|
118
118
|
throw new Error(`${path}: exceeds the 512 MB limit`);
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
+
function validateSizeKbLimit(kb, path) {
|
|
122
|
+
validateInMemoryByteLimit(kb * 1024, path);
|
|
123
|
+
}
|
|
121
124
|
function validateLargeTargetBytes(value, path) {
|
|
122
125
|
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
123
126
|
throw new Error(`${path}: must be an integer; got ${JSON.stringify(value)}`);
|
|
@@ -155,6 +158,29 @@ function validateLargeImageEntry(e, path) {
|
|
|
155
158
|
}
|
|
156
159
|
if ("tags" in e) validateTags(e.tags, `${path}.tags`);
|
|
157
160
|
}
|
|
161
|
+
function validateNonDicomEntry(e, path) {
|
|
162
|
+
const sizing = ["content", "targetSizeKb", "targetBytes"].filter(
|
|
163
|
+
(f) => f in e
|
|
164
|
+
);
|
|
165
|
+
if (sizing.length > 1) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`${path}: "content", "targetSizeKb" and "targetBytes" are mutually exclusive`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if ("content" in e && typeof e.content !== "string") {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`${path}.content: must be a string; got ${JSON.stringify(e.content)}`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if ("targetSizeKb" in e) {
|
|
176
|
+
validatePositiveInt(e.targetSizeKb, `${path}.targetSizeKb`);
|
|
177
|
+
validateSizeKbLimit(e.targetSizeKb, `${path}.targetSizeKb`);
|
|
178
|
+
}
|
|
179
|
+
if ("targetBytes" in e) {
|
|
180
|
+
validatePositiveInt(e.targetBytes, `${path}.targetBytes`);
|
|
181
|
+
validateInMemoryByteLimit(e.targetBytes, `${path}.targetBytes`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
158
184
|
function validateEnum(value, valid, path) {
|
|
159
185
|
if (typeof value !== "string" || !Object.hasOwn(valid, value)) {
|
|
160
186
|
throw new Error(
|
|
@@ -182,6 +208,22 @@ function validateEntry(entry, path) {
|
|
|
182
208
|
validateLargeImageEntry(e, path);
|
|
183
209
|
return e;
|
|
184
210
|
}
|
|
211
|
+
if (type === "non-dicom") {
|
|
212
|
+
for (const field of [
|
|
213
|
+
"modality",
|
|
214
|
+
"rows",
|
|
215
|
+
"columns",
|
|
216
|
+
"frames",
|
|
217
|
+
"tags",
|
|
218
|
+
"violations",
|
|
219
|
+
"transferSyntax"
|
|
220
|
+
]) {
|
|
221
|
+
if (field in e)
|
|
222
|
+
throw new Error(`${path}.${field}: not supported for type "${type}"`);
|
|
223
|
+
}
|
|
224
|
+
validateNonDicomEntry(e, path);
|
|
225
|
+
return e;
|
|
226
|
+
}
|
|
185
227
|
if ("targetBytes" in e) {
|
|
186
228
|
throw new Error(
|
|
187
229
|
`${path}.targetBytes: only supported for type "large-image"`
|
|
@@ -389,6 +431,101 @@ function validateStudy(study, path) {
|
|
|
389
431
|
if ("tags" in st) validateTags(st.tags, `${path}.tags`);
|
|
390
432
|
return st;
|
|
391
433
|
}
|
|
434
|
+
function positionalName(prefix, ordinal) {
|
|
435
|
+
return `${prefix}-${String(ordinal).padStart(3, "0")}`;
|
|
436
|
+
}
|
|
437
|
+
var VALID_TREE_ROLES = {
|
|
438
|
+
study: true,
|
|
439
|
+
series: true
|
|
440
|
+
};
|
|
441
|
+
function validateNodeName(value, path) {
|
|
442
|
+
if (typeof value !== "string" || value.length === 0 || value === "." || value === ".." || /[/\\]/.test(value)) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
`${path}: must be a non-empty name without path separators; got ${JSON.stringify(value)}`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function validateTreeNode(node, path, inStudy, inSeries) {
|
|
449
|
+
if (typeof node !== "object" || node === null || Array.isArray(node)) {
|
|
450
|
+
throw new Error(`${path}: must be an object`);
|
|
451
|
+
}
|
|
452
|
+
const n = node;
|
|
453
|
+
if ("children" in n) {
|
|
454
|
+
if ("type" in n) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
`${path}: a node cannot have both "children" (directory) and "type" (file)`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
if ("name" in n) validateNodeName(n.name, `${path}.name`);
|
|
460
|
+
if ("tags" in n) validateTags(n.tags, `${path}.tags`);
|
|
461
|
+
let childStudy = inStudy;
|
|
462
|
+
let childSeries = inSeries;
|
|
463
|
+
if ("role" in n) {
|
|
464
|
+
validateEnum(n.role, VALID_TREE_ROLES, `${path}.role`);
|
|
465
|
+
if (n.role === "study") {
|
|
466
|
+
if (inStudy) {
|
|
467
|
+
throw new Error(
|
|
468
|
+
`${path}.role: a study directory cannot nest inside another study`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
childStudy = true;
|
|
472
|
+
} else {
|
|
473
|
+
if (!inStudy) {
|
|
474
|
+
throw new Error(
|
|
475
|
+
`${path}.role: a series directory requires an enclosing study-role directory`
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
if (inSeries) {
|
|
479
|
+
throw new Error(
|
|
480
|
+
`${path}.role: a series directory cannot nest inside another series`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
childSeries = true;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (!Array.isArray(n.children) || n.children.length === 0) {
|
|
487
|
+
throw new Error(`${path}.children: must be a non-empty array`);
|
|
488
|
+
}
|
|
489
|
+
validateTreeChildren(
|
|
490
|
+
n.children,
|
|
491
|
+
`${path}.children`,
|
|
492
|
+
childStudy,
|
|
493
|
+
childSeries
|
|
494
|
+
);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const e = validateEntry(node, path);
|
|
498
|
+
if ("name" in n) {
|
|
499
|
+
validateNodeName(n.name, `${path}.name`);
|
|
500
|
+
if ((e.count ?? 1) > 1) {
|
|
501
|
+
throw new Error(
|
|
502
|
+
`${path}: "name" cannot be combined with count > 1 \u2014 the copies would collide on one path`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function validateTreeChildren(children, basePath, inStudy, inSeries) {
|
|
508
|
+
const claimed = /* @__PURE__ */ new Map();
|
|
509
|
+
let dirOrdinal = 0;
|
|
510
|
+
children.forEach((child, i) => {
|
|
511
|
+
const path = `${basePath}[${i}]`;
|
|
512
|
+
validateTreeNode(child, path, inStudy, inSeries);
|
|
513
|
+
const n = child;
|
|
514
|
+
let name = "name" in n ? n.name : void 0;
|
|
515
|
+
if ("children" in n) {
|
|
516
|
+
dirOrdinal++;
|
|
517
|
+
name ?? (name = positionalName("dir", dirOrdinal));
|
|
518
|
+
}
|
|
519
|
+
if (name === void 0) return;
|
|
520
|
+
const prior = claimed.get(name);
|
|
521
|
+
if (prior !== void 0) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
`${path}: name "${name}" collides with ${prior} \u2014 siblings must resolve to distinct paths`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
claimed.set(name, path);
|
|
527
|
+
});
|
|
528
|
+
}
|
|
392
529
|
function validateDatasetSpec(raw) {
|
|
393
530
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
394
531
|
throw new Error("DatasetSpec: must be a JSON object");
|
|
@@ -400,11 +537,15 @@ function validateDatasetSpec(raw) {
|
|
|
400
537
|
if ("studies" in r && !Array.isArray(r.studies)) {
|
|
401
538
|
throw new Error("DatasetSpec.studies: must be an array");
|
|
402
539
|
}
|
|
540
|
+
if ("tree" in r && !Array.isArray(r.tree)) {
|
|
541
|
+
throw new Error("DatasetSpec.tree: must be an array");
|
|
542
|
+
}
|
|
403
543
|
const rawEntries = r.entries ?? [];
|
|
404
544
|
const rawStudies = r.studies ?? [];
|
|
405
|
-
|
|
545
|
+
const rawTree = r.tree ?? [];
|
|
546
|
+
if (rawEntries.length === 0 && rawStudies.length === 0 && rawTree.length === 0) {
|
|
406
547
|
throw new Error(
|
|
407
|
-
'DatasetSpec: must contain at least one of "entries" or "
|
|
548
|
+
'DatasetSpec: must contain at least one of "entries", "studies" or "tree" (non-empty)'
|
|
408
549
|
);
|
|
409
550
|
}
|
|
410
551
|
const entries = rawEntries.map(
|
|
@@ -413,6 +554,9 @@ function validateDatasetSpec(raw) {
|
|
|
413
554
|
const studies = rawStudies.map(
|
|
414
555
|
(study, i) => validateStudy(study, `studies[${i}]`)
|
|
415
556
|
);
|
|
557
|
+
if (rawTree.length > 0) {
|
|
558
|
+
validateTreeChildren(rawTree, "tree", false, false);
|
|
559
|
+
}
|
|
416
560
|
if ("seed" in r) validateSeed(r.seed, "DatasetSpec.seed");
|
|
417
561
|
if ("uidSalt" in r) validateUidSalt(r.uidSalt, "DatasetSpec.uidSalt");
|
|
418
562
|
if ("layout" in r) {
|
|
@@ -429,6 +573,7 @@ function validateDatasetSpec(raw) {
|
|
|
429
573
|
return {
|
|
430
574
|
...entries.length > 0 ? { entries } : {},
|
|
431
575
|
...studies.length > 0 ? { studies } : {},
|
|
576
|
+
...rawTree.length > 0 ? { tree: r.tree } : {},
|
|
432
577
|
...r.seed !== void 0 ? { seed: r.seed } : {},
|
|
433
578
|
...r.uidSalt !== void 0 ? { uidSalt: r.uidSalt } : {},
|
|
434
579
|
...r.layout !== void 0 ? { layout: r.layout } : {},
|
|
@@ -708,6 +853,37 @@ function mergeTags(a, b) {
|
|
|
708
853
|
if (!b) return a;
|
|
709
854
|
return { ...a, ...b };
|
|
710
855
|
}
|
|
856
|
+
function scanFile(path) {
|
|
857
|
+
let size;
|
|
858
|
+
try {
|
|
859
|
+
size = statSync(path).size;
|
|
860
|
+
} catch {
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
if (size > MAX_STREAM_BYTES) return null;
|
|
864
|
+
const large = size > MAX_PIXEL_BYTES;
|
|
865
|
+
return { size, large, record: large ? readHeaderOnly(path) : parseFull(path) };
|
|
866
|
+
}
|
|
867
|
+
function presetModalityOf(record) {
|
|
868
|
+
if (typeof record.Modality !== "string") return void 0;
|
|
869
|
+
return SUPPORTED_MODALITIES.has(record.Modality) ? record.Modality : "unsupported";
|
|
870
|
+
}
|
|
871
|
+
function hashRecordUids(rec, salt, uidMap, sweptKeepTags) {
|
|
872
|
+
if (!rec.uidOriginals) return;
|
|
873
|
+
if (rec.tags) {
|
|
874
|
+
for (const keyword of Object.keys(rec.uidOriginals)) {
|
|
875
|
+
if (keyword in rec.tags) sweptKeepTags.add(keyword);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
rec.tags = mergeTags(rec.tags, hashUidTags(rec.uidOriginals, salt, uidMap));
|
|
879
|
+
}
|
|
880
|
+
function warnSweptKeepTags(sweptKeepTags) {
|
|
881
|
+
for (const keyword of sweptKeepTags) {
|
|
882
|
+
console.warn(
|
|
883
|
+
`describe: keep-tag "${keyword}" contains UID references \u2014 kept as its hashed UID skeleton, not verbatim (disable with preserveUids: false)`
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
711
887
|
function sizeKbOf(bytes) {
|
|
712
888
|
return Math.max(1, Math.round(bytes / 1024));
|
|
713
889
|
}
|
|
@@ -810,9 +986,128 @@ function buildSeriesSpec(records) {
|
|
|
810
986
|
function seriesSpecFromAccum(accum) {
|
|
811
987
|
return Array.isArray(accum) ? buildSeriesSpec(accum) : { entries: [...accum.values()].map(entryOf) };
|
|
812
988
|
}
|
|
989
|
+
function dirAccumFor(root, dirs) {
|
|
990
|
+
let node = root;
|
|
991
|
+
for (const name of dirs) {
|
|
992
|
+
let child = node.dirs.get(name);
|
|
993
|
+
if (!child) {
|
|
994
|
+
child = { dirs: /* @__PURE__ */ new Map(), files: [] };
|
|
995
|
+
node.dirs.set(name, child);
|
|
996
|
+
}
|
|
997
|
+
node = child;
|
|
998
|
+
}
|
|
999
|
+
return node;
|
|
1000
|
+
}
|
|
1001
|
+
var SAFE_EXTENSION_RE = /^\.[A-Za-z0-9]{1,8}$/;
|
|
1002
|
+
function safeExtension(path) {
|
|
1003
|
+
const ext = extname(basename(path));
|
|
1004
|
+
return SAFE_EXTENSION_RE.test(ext) ? ext : "";
|
|
1005
|
+
}
|
|
1006
|
+
function emitTree(acc) {
|
|
1007
|
+
const nodes = [];
|
|
1008
|
+
let otherOrdinal = 0;
|
|
1009
|
+
for (const leaf of acc.files) {
|
|
1010
|
+
if (leaf.kind === "other") {
|
|
1011
|
+
otherOrdinal++;
|
|
1012
|
+
const name = `${positionalName("file", otherOrdinal)}${leaf.ext}`;
|
|
1013
|
+
nodes.push(
|
|
1014
|
+
leaf.bytes > 0 ? { type: "non-dicom", name, targetBytes: leaf.bytes } : { type: "non-dicom", name, content: "" }
|
|
1015
|
+
);
|
|
1016
|
+
} else {
|
|
1017
|
+
nodes.push(entryOf({ ...leaf.rec, count: 1 }));
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
for (const sub of acc.dirs.values()) {
|
|
1021
|
+
nodes.push({ children: emitTree(sub) });
|
|
1022
|
+
}
|
|
1023
|
+
return nodes;
|
|
1024
|
+
}
|
|
1025
|
+
function describeTree(dir, options, keepKeywords) {
|
|
1026
|
+
const root = { dirs: /* @__PURE__ */ new Map(), files: [] };
|
|
1027
|
+
const stats = {
|
|
1028
|
+
dicomFiles: 0,
|
|
1029
|
+
skipped: 0,
|
|
1030
|
+
unknownModality: 0,
|
|
1031
|
+
nonDicomFiles: 0
|
|
1032
|
+
};
|
|
1033
|
+
const studyUids = /* @__PURE__ */ new Set();
|
|
1034
|
+
const dicomLeaves = [];
|
|
1035
|
+
for (const path of [...walkFiles(dir)].sort()) {
|
|
1036
|
+
const scanned = scanFile(path);
|
|
1037
|
+
if (!scanned) {
|
|
1038
|
+
stats.skipped++;
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
const { size, large, record } = scanned;
|
|
1042
|
+
const studyUid = record?.StudyInstanceUID;
|
|
1043
|
+
const seriesUid = record?.SeriesInstanceUID;
|
|
1044
|
+
const relDirs = relative(dir, path).split(sep).slice(0, -1);
|
|
1045
|
+
if (!record || typeof studyUid !== "string" || typeof seriesUid !== "string") {
|
|
1046
|
+
if (large) {
|
|
1047
|
+
stats.skipped++;
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
stats.nonDicomFiles++;
|
|
1051
|
+
dirAccumFor(root, relDirs).files.push({
|
|
1052
|
+
kind: "other",
|
|
1053
|
+
bytes: size,
|
|
1054
|
+
ext: safeExtension(path)
|
|
1055
|
+
});
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
stats.dicomFiles++;
|
|
1059
|
+
studyUids.add(studyUid);
|
|
1060
|
+
const preset = presetModalityOf(record);
|
|
1061
|
+
if (preset === "unsupported") stats.unknownModality++;
|
|
1062
|
+
const modality = preset === "unsupported" ? void 0 : preset;
|
|
1063
|
+
const instanceNumber = typeof record.InstanceNumber === "number" ? { InstanceNumber: record.InstanceNumber } : typeof record.InstanceNumber === "string" ? { InstanceNumber: escapeTagTemplate(record.InstanceNumber) } : void 0;
|
|
1064
|
+
const leaf = {
|
|
1065
|
+
kind: "dicom",
|
|
1066
|
+
rec: {
|
|
1067
|
+
large,
|
|
1068
|
+
bytes: size,
|
|
1069
|
+
modality,
|
|
1070
|
+
tags: mergeTags(
|
|
1071
|
+
instanceNumber,
|
|
1072
|
+
keepKeywords.length ? extractTags(record, keepKeywords) : void 0
|
|
1073
|
+
),
|
|
1074
|
+
uidOriginals: collectUidOriginals(record)
|
|
1075
|
+
},
|
|
1076
|
+
studyUid,
|
|
1077
|
+
seriesUid
|
|
1078
|
+
};
|
|
1079
|
+
dirAccumFor(root, relDirs).files.push(leaf);
|
|
1080
|
+
dicomLeaves.push(leaf);
|
|
1081
|
+
}
|
|
1082
|
+
if (root.files.length === 0 && root.dirs.size === 0) {
|
|
1083
|
+
throw new Error(`describe: no files found in ${dir}`);
|
|
1084
|
+
}
|
|
1085
|
+
const salt = options.uidSalt ?? deriveSalt([...studyUids]);
|
|
1086
|
+
const uidMap = /* @__PURE__ */ new Map();
|
|
1087
|
+
const sweptKeepTags = /* @__PURE__ */ new Set();
|
|
1088
|
+
for (const { rec, studyUid, seriesUid } of dicomLeaves) {
|
|
1089
|
+
hashRecordUids(rec, salt, uidMap, sweptKeepTags);
|
|
1090
|
+
rec.tags = mergeTags(rec.tags, {
|
|
1091
|
+
StudyInstanceUID: hashUidVia(uidMap, salt, studyUid),
|
|
1092
|
+
SeriesInstanceUID: hashUidVia(uidMap, salt, seriesUid)
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
warnSweptKeepTags(sweptKeepTags);
|
|
1096
|
+
const spec = { tree: emitTree(root), uidSalt: salt };
|
|
1097
|
+
validateDatasetSpec(spec);
|
|
1098
|
+
return { spec, stats };
|
|
1099
|
+
}
|
|
813
1100
|
function describeDirectory(dir, options = {}) {
|
|
814
1101
|
const keepKeywords = options.keepTags ? resolveKeepTags(options.keepTags) : [];
|
|
815
1102
|
const preserveUids = options.preserveUids ?? true;
|
|
1103
|
+
if (options.captureTree) {
|
|
1104
|
+
if (!preserveUids) {
|
|
1105
|
+
throw new Error(
|
|
1106
|
+
"describe: captureTree requires preserveUids \u2014 the captured tree carries its grouping as hashed UID tags"
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
return describeTree(dir, options, keepKeywords);
|
|
1110
|
+
}
|
|
816
1111
|
const uidMap = /* @__PURE__ */ new Map();
|
|
817
1112
|
const useRecords = keepKeywords.length > 0 || preserveUids;
|
|
818
1113
|
const tolerance = options.sizeTolerance ?? 0;
|
|
@@ -822,21 +1117,19 @@ function describeDirectory(dir, options = {}) {
|
|
|
822
1117
|
);
|
|
823
1118
|
}
|
|
824
1119
|
const studies = /* @__PURE__ */ new Map();
|
|
825
|
-
const stats = {
|
|
1120
|
+
const stats = {
|
|
1121
|
+
dicomFiles: 0,
|
|
1122
|
+
skipped: 0,
|
|
1123
|
+
unknownModality: 0,
|
|
1124
|
+
nonDicomFiles: 0
|
|
1125
|
+
};
|
|
826
1126
|
for (const path of [...walkFiles(dir)].sort()) {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
size = statSync(path).size;
|
|
830
|
-
} catch {
|
|
831
|
-
stats.skipped++;
|
|
832
|
-
continue;
|
|
833
|
-
}
|
|
834
|
-
if (size > MAX_STREAM_BYTES) {
|
|
1127
|
+
const scanned = scanFile(path);
|
|
1128
|
+
if (!scanned) {
|
|
835
1129
|
stats.skipped++;
|
|
836
1130
|
continue;
|
|
837
1131
|
}
|
|
838
|
-
const large
|
|
839
|
-
const record = large ? readHeaderOnly(path) : parseFull(path);
|
|
1132
|
+
const { size, large, record } = scanned;
|
|
840
1133
|
const studyUid = record?.StudyInstanceUID;
|
|
841
1134
|
const seriesUid = record?.SeriesInstanceUID;
|
|
842
1135
|
if (!record || typeof studyUid !== "string" || typeof seriesUid !== "string") {
|
|
@@ -844,14 +1137,9 @@ function describeDirectory(dir, options = {}) {
|
|
|
844
1137
|
continue;
|
|
845
1138
|
}
|
|
846
1139
|
stats.dicomFiles++;
|
|
847
|
-
|
|
848
|
-
if (
|
|
849
|
-
|
|
850
|
-
modality = record.Modality;
|
|
851
|
-
} else {
|
|
852
|
-
stats.unknownModality++;
|
|
853
|
-
}
|
|
854
|
-
}
|
|
1140
|
+
const preset = presetModalityOf(record);
|
|
1141
|
+
if (preset === "unsupported") stats.unknownModality++;
|
|
1142
|
+
const modality = preset === "unsupported" ? void 0 : preset;
|
|
855
1143
|
const keptTags = keepKeywords.length ? extractTags(record, keepKeywords) : void 0;
|
|
856
1144
|
const uidOriginals = preserveUids ? collectUidOriginals(record) : void 0;
|
|
857
1145
|
const bytes = large ? size : Math.min(bucketBytes(size, tolerance), MAX_PIXEL_BYTES);
|
|
@@ -885,25 +1173,11 @@ function describeDirectory(dir, options = {}) {
|
|
|
885
1173
|
for (const series of study.values()) {
|
|
886
1174
|
if (!Array.isArray(series)) continue;
|
|
887
1175
|
for (const rec of series) {
|
|
888
|
-
|
|
889
|
-
if (rec.tags) {
|
|
890
|
-
for (const keyword of Object.keys(rec.uidOriginals)) {
|
|
891
|
-
if (keyword in rec.tags) sweptKeepTags.add(keyword);
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
rec.tags = mergeTags(
|
|
895
|
-
rec.tags,
|
|
896
|
-
hashUidTags(rec.uidOriginals, salt, uidMap)
|
|
897
|
-
);
|
|
898
|
-
}
|
|
1176
|
+
hashRecordUids(rec, salt, uidMap, sweptKeepTags);
|
|
899
1177
|
}
|
|
900
1178
|
}
|
|
901
1179
|
}
|
|
902
|
-
|
|
903
|
-
console.warn(
|
|
904
|
-
`describe: keep-tag "${keyword}" contains UID references \u2014 kept as its hashed UID skeleton, not verbatim (disable with preserveUids: false)`
|
|
905
|
-
);
|
|
906
|
-
}
|
|
1180
|
+
warnSweptKeepTags(sweptKeepTags);
|
|
907
1181
|
}
|
|
908
1182
|
const withGroupUid = (spec2, seriesUid) => salt === void 0 ? spec2 : {
|
|
909
1183
|
...spec2,
|