dicom-synth 1.15.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 +15 -1
- package/dist/esm/describe/describe.js +169 -37
- package/dist/esm/index.js +181 -38
- package/dist/esm/schema/validate.js +5 -1
- package/dist/esm/syntheticFixtures/generator.js +11 -1
- package/dist/esm/syntheticFixtures/streamWrite.js +10 -0
- package/dist/types/describe/describe.d.ts +2 -0
- package/dist/types/schema/validate.d.ts +1 -0
- package/dist/types/syntheticFixtures/generator.d.ts +1 -0
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { describeDirectory } from '../dist/esm/index.js'
|
|
|
6
6
|
|
|
7
7
|
function usage() {
|
|
8
8
|
console.error(
|
|
9
|
-
'Usage: dicom-synth-describe <dir> [--out <spec.json[.gz]>] [--keep-tags <a,b,...>] [--keep-tags-file <path>] [--size-tolerance <fraction>] [--no-preserve-uids] [--uid-salt <hex>] [--gzip]\n' +
|
|
9
|
+
'Usage: dicom-synth-describe <dir> [--out <spec.json[.gz]>] [--tree] [--keep-tags <a,b,...>] [--keep-tags-file <path>] [--size-tolerance <fraction>] [--no-preserve-uids] [--uid-salt <hex>] [--gzip]\n' +
|
|
10
10
|
'\n' +
|
|
11
11
|
'Scans a DICOM tree and emits a DatasetSpec describing its shape\n' +
|
|
12
12
|
'(studies/series, per-file size, modality).\n' +
|
|
@@ -14,6 +14,9 @@ function usage() {
|
|
|
14
14
|
'UIDs are preserved by default as hash-derived synthetic UIDs, keeping\n' +
|
|
15
15
|
'cross-file links (FrameOfReferenceUID etc.) without emitting any original\n' +
|
|
16
16
|
'UID. Other tags are not copied unless named.\n' +
|
|
17
|
+
'--tree captures the real directory structure as a generic tree spec\n' +
|
|
18
|
+
'(anonymised names, sized non-DICOM placeholders included) instead of\n' +
|
|
19
|
+
'the compact studies/series form. Requires UID preservation.\n' +
|
|
17
20
|
'--keep-tags copies the named tags (DICOM keywords or 8-hex tags)\n' +
|
|
18
21
|
"verbatim; if a kept tag holds PHI, handling it is the caller's\n" +
|
|
19
22
|
"responsibility, not this tool's.\n" +
|
|
@@ -48,6 +51,7 @@ let sizeTolerance
|
|
|
48
51
|
let preserveUids = true
|
|
49
52
|
let uidSalt
|
|
50
53
|
let gzip = false
|
|
54
|
+
let captureTree = false
|
|
51
55
|
const keepTags = []
|
|
52
56
|
|
|
53
57
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -75,6 +79,8 @@ for (let i = 0; i < args.length; i++) {
|
|
|
75
79
|
}
|
|
76
80
|
} else if (args[i] === '--no-preserve-uids') {
|
|
77
81
|
preserveUids = false
|
|
82
|
+
} else if (args[i] === '--tree') {
|
|
83
|
+
captureTree = true
|
|
78
84
|
} else if (args[i] === '--uid-salt' && args[i + 1]) {
|
|
79
85
|
uidSalt = args[++i]
|
|
80
86
|
} else if (args[i] === '--gzip') {
|
|
@@ -104,6 +110,7 @@ try {
|
|
|
104
110
|
...(sizeTolerance !== undefined ? { sizeTolerance } : {}),
|
|
105
111
|
preserveUids,
|
|
106
112
|
...(uidSalt !== undefined ? { uidSalt } : {}),
|
|
113
|
+
...(captureTree ? { captureTree: true } : {}),
|
|
107
114
|
})
|
|
108
115
|
} catch (err) {
|
|
109
116
|
console.error(`Error: ${err.message}`)
|
|
@@ -119,10 +126,15 @@ if (outPath) {
|
|
|
119
126
|
process.stdout.write(compress ? gzipSync(json) : json)
|
|
120
127
|
}
|
|
121
128
|
|
|
122
|
-
const { dicomFiles, skipped, unknownModality } = result.stats
|
|
129
|
+
const { dicomFiles, skipped, unknownModality, nonDicomFiles } = result.stats
|
|
123
130
|
console.error(
|
|
124
131
|
`Described ${dicomFiles} DICOM file(s); skipped ${skipped} non-DICOM/ungroupable file(s)`,
|
|
125
132
|
)
|
|
133
|
+
if (nonDicomFiles > 0) {
|
|
134
|
+
console.error(
|
|
135
|
+
` ${nonDicomFiles} non-DICOM file(s) captured as sized placeholders`,
|
|
136
|
+
)
|
|
137
|
+
}
|
|
126
138
|
if (result.spec.uidSalt) {
|
|
127
139
|
console.error(
|
|
128
140
|
` UIDs hash-derived with salt ${result.spec.uidSalt} (reuse via --uid-salt to reproduce)`,
|
|
@@ -56,6 +56,9 @@ function isImageType(type) {
|
|
|
56
56
|
return TYPE_CAPABILITIES[type].image;
|
|
57
57
|
}
|
|
58
58
|
var HEX_TAG_RE = /^[0-9a-fA-F]{8}$/;
|
|
59
|
+
function positionalName(prefix, ordinal) {
|
|
60
|
+
return `${prefix}-${String(ordinal).padStart(3, "0")}`;
|
|
61
|
+
}
|
|
59
62
|
|
|
60
63
|
// src/loadDcmjs.ts
|
|
61
64
|
import dcmjsDefaultImport, * as dcmjsNamespace from "dcmjs";
|
|
@@ -160,6 +163,14 @@ function applyTagOverrides(dataset, tags) {
|
|
|
160
163
|
}
|
|
161
164
|
}
|
|
162
165
|
}
|
|
166
|
+
function syncMetaWithDataset(meta, dataset) {
|
|
167
|
+
if (typeof dataset.SOPInstanceUID === "string") {
|
|
168
|
+
meta.MediaStorageSOPInstanceUID = dataset.SOPInstanceUID;
|
|
169
|
+
}
|
|
170
|
+
if (typeof dataset.SOPClassUID === "string") {
|
|
171
|
+
meta.MediaStorageSOPClassUID = dataset.SOPClassUID;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
163
174
|
function buildMeta(uid, modality, transferSyntax = "explicit-vr-little-endian") {
|
|
164
175
|
return {
|
|
165
176
|
FileMetaInformationVersion: new Uint8Array([0, 1]).buffer,
|
|
@@ -246,6 +257,7 @@ function buildImageBuffer(spec, uid) {
|
|
|
246
257
|
}
|
|
247
258
|
applyTagOverrides(dataset, spec.tags);
|
|
248
259
|
const meta = buildMeta(effectiveUid, modality, spec.transferSyntax);
|
|
260
|
+
syncMetaWithDataset(meta, dataset);
|
|
249
261
|
applySizing(dataset, meta, spec);
|
|
250
262
|
return serializeDict(meta, dataset);
|
|
251
263
|
}
|
|
@@ -355,6 +367,7 @@ function writeNative(outPath, params, dims, zeroChunk) {
|
|
|
355
367
|
const meta = buildMeta(uid, modality);
|
|
356
368
|
const dataset = buildBaseImageDataset(uid, modality);
|
|
357
369
|
applyTagOverrides(dataset, tags);
|
|
370
|
+
syncMetaWithDataset(meta, dataset);
|
|
358
371
|
dataset.Rows = dims.rows;
|
|
359
372
|
dataset.Columns = dims.columns;
|
|
360
373
|
const minimalPixelBytes = dataset.PixelData.byteLength;
|
|
@@ -388,6 +401,7 @@ function writeEncapsulated(outPath, params, pixelBytes, fragmentBytes, zeroChunk
|
|
|
388
401
|
};
|
|
389
402
|
const dataset = buildBaseImageDataset(uid, modality);
|
|
390
403
|
applyTagOverrides(dataset, tags);
|
|
404
|
+
syncMetaWithDataset(meta, dataset);
|
|
391
405
|
dataset.PixelData = [new Uint8Array([]).buffer, new Uint8Array([0, 0]).buffer];
|
|
392
406
|
dataset._vrMap = { PixelData: "OB" };
|
|
393
407
|
const probe = serializeDict(meta, dataset);
|
|
@@ -723,7 +737,7 @@ function* planTree(nodes, scope, env) {
|
|
|
723
737
|
for (const node of nodes) {
|
|
724
738
|
if (isDirNode(node)) {
|
|
725
739
|
dirOrdinal++;
|
|
726
|
-
const dirName = node.name ??
|
|
740
|
+
const dirName = node.name ?? positionalName("dir", dirOrdinal);
|
|
727
741
|
const child = {
|
|
728
742
|
...scope,
|
|
729
743
|
dirs: [...scope.dirs, dirName],
|
|
@@ -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";
|
|
@@ -431,6 +431,9 @@ function validateStudy(study, path) {
|
|
|
431
431
|
if ("tags" in st) validateTags(st.tags, `${path}.tags`);
|
|
432
432
|
return st;
|
|
433
433
|
}
|
|
434
|
+
function positionalName(prefix, ordinal) {
|
|
435
|
+
return `${prefix}-${String(ordinal).padStart(3, "0")}`;
|
|
436
|
+
}
|
|
434
437
|
var VALID_TREE_ROLES = {
|
|
435
438
|
study: true,
|
|
436
439
|
series: true
|
|
@@ -511,7 +514,7 @@ function validateTreeChildren(children, basePath, inStudy, inSeries) {
|
|
|
511
514
|
let name = "name" in n ? n.name : void 0;
|
|
512
515
|
if ("children" in n) {
|
|
513
516
|
dirOrdinal++;
|
|
514
|
-
name ?? (name =
|
|
517
|
+
name ?? (name = positionalName("dir", dirOrdinal));
|
|
515
518
|
}
|
|
516
519
|
if (name === void 0) return;
|
|
517
520
|
const prior = claimed.get(name);
|
|
@@ -850,6 +853,37 @@ function mergeTags(a, b) {
|
|
|
850
853
|
if (!b) return a;
|
|
851
854
|
return { ...a, ...b };
|
|
852
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
|
+
}
|
|
853
887
|
function sizeKbOf(bytes) {
|
|
854
888
|
return Math.max(1, Math.round(bytes / 1024));
|
|
855
889
|
}
|
|
@@ -952,9 +986,128 @@ function buildSeriesSpec(records) {
|
|
|
952
986
|
function seriesSpecFromAccum(accum) {
|
|
953
987
|
return Array.isArray(accum) ? buildSeriesSpec(accum) : { entries: [...accum.values()].map(entryOf) };
|
|
954
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
|
+
}
|
|
955
1100
|
function describeDirectory(dir, options = {}) {
|
|
956
1101
|
const keepKeywords = options.keepTags ? resolveKeepTags(options.keepTags) : [];
|
|
957
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
|
+
}
|
|
958
1111
|
const uidMap = /* @__PURE__ */ new Map();
|
|
959
1112
|
const useRecords = keepKeywords.length > 0 || preserveUids;
|
|
960
1113
|
const tolerance = options.sizeTolerance ?? 0;
|
|
@@ -964,21 +1117,19 @@ function describeDirectory(dir, options = {}) {
|
|
|
964
1117
|
);
|
|
965
1118
|
}
|
|
966
1119
|
const studies = /* @__PURE__ */ new Map();
|
|
967
|
-
const stats = {
|
|
1120
|
+
const stats = {
|
|
1121
|
+
dicomFiles: 0,
|
|
1122
|
+
skipped: 0,
|
|
1123
|
+
unknownModality: 0,
|
|
1124
|
+
nonDicomFiles: 0
|
|
1125
|
+
};
|
|
968
1126
|
for (const path of [...walkFiles(dir)].sort()) {
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
size = statSync(path).size;
|
|
972
|
-
} catch {
|
|
1127
|
+
const scanned = scanFile(path);
|
|
1128
|
+
if (!scanned) {
|
|
973
1129
|
stats.skipped++;
|
|
974
1130
|
continue;
|
|
975
1131
|
}
|
|
976
|
-
|
|
977
|
-
stats.skipped++;
|
|
978
|
-
continue;
|
|
979
|
-
}
|
|
980
|
-
const large = size > MAX_PIXEL_BYTES;
|
|
981
|
-
const record = large ? readHeaderOnly(path) : parseFull(path);
|
|
1132
|
+
const { size, large, record } = scanned;
|
|
982
1133
|
const studyUid = record?.StudyInstanceUID;
|
|
983
1134
|
const seriesUid = record?.SeriesInstanceUID;
|
|
984
1135
|
if (!record || typeof studyUid !== "string" || typeof seriesUid !== "string") {
|
|
@@ -986,14 +1137,9 @@ function describeDirectory(dir, options = {}) {
|
|
|
986
1137
|
continue;
|
|
987
1138
|
}
|
|
988
1139
|
stats.dicomFiles++;
|
|
989
|
-
|
|
990
|
-
if (
|
|
991
|
-
|
|
992
|
-
modality = record.Modality;
|
|
993
|
-
} else {
|
|
994
|
-
stats.unknownModality++;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
1140
|
+
const preset = presetModalityOf(record);
|
|
1141
|
+
if (preset === "unsupported") stats.unknownModality++;
|
|
1142
|
+
const modality = preset === "unsupported" ? void 0 : preset;
|
|
997
1143
|
const keptTags = keepKeywords.length ? extractTags(record, keepKeywords) : void 0;
|
|
998
1144
|
const uidOriginals = preserveUids ? collectUidOriginals(record) : void 0;
|
|
999
1145
|
const bytes = large ? size : Math.min(bucketBytes(size, tolerance), MAX_PIXEL_BYTES);
|
|
@@ -1027,25 +1173,11 @@ function describeDirectory(dir, options = {}) {
|
|
|
1027
1173
|
for (const series of study.values()) {
|
|
1028
1174
|
if (!Array.isArray(series)) continue;
|
|
1029
1175
|
for (const rec of series) {
|
|
1030
|
-
|
|
1031
|
-
if (rec.tags) {
|
|
1032
|
-
for (const keyword of Object.keys(rec.uidOriginals)) {
|
|
1033
|
-
if (keyword in rec.tags) sweptKeepTags.add(keyword);
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
rec.tags = mergeTags(
|
|
1037
|
-
rec.tags,
|
|
1038
|
-
hashUidTags(rec.uidOriginals, salt, uidMap)
|
|
1039
|
-
);
|
|
1040
|
-
}
|
|
1176
|
+
hashRecordUids(rec, salt, uidMap, sweptKeepTags);
|
|
1041
1177
|
}
|
|
1042
1178
|
}
|
|
1043
1179
|
}
|
|
1044
|
-
|
|
1045
|
-
console.warn(
|
|
1046
|
-
`describe: keep-tag "${keyword}" contains UID references \u2014 kept as its hashed UID skeleton, not verbatim (disable with preserveUids: false)`
|
|
1047
|
-
);
|
|
1048
|
-
}
|
|
1180
|
+
warnSweptKeepTags(sweptKeepTags);
|
|
1049
1181
|
}
|
|
1050
1182
|
const withGroupUid = (spec2, seriesUid) => salt === void 0 ? spec2 : {
|
|
1051
1183
|
...spec2,
|
package/dist/esm/index.js
CHANGED
|
@@ -441,6 +441,9 @@ function validateStudy(study, path) {
|
|
|
441
441
|
if ("tags" in st) validateTags(st.tags, `${path}.tags`);
|
|
442
442
|
return st;
|
|
443
443
|
}
|
|
444
|
+
function positionalName(prefix, ordinal) {
|
|
445
|
+
return `${prefix}-${String(ordinal).padStart(3, "0")}`;
|
|
446
|
+
}
|
|
444
447
|
var VALID_TREE_ROLES = {
|
|
445
448
|
study: true,
|
|
446
449
|
series: true
|
|
@@ -521,7 +524,7 @@ function validateTreeChildren(children, basePath, inStudy, inSeries) {
|
|
|
521
524
|
let name = "name" in n ? n.name : void 0;
|
|
522
525
|
if ("children" in n) {
|
|
523
526
|
dirOrdinal++;
|
|
524
|
-
name ?? (name =
|
|
527
|
+
name ?? (name = positionalName("dir", dirOrdinal));
|
|
525
528
|
}
|
|
526
529
|
if (name === void 0) return;
|
|
527
530
|
const prior = claimed.get(name);
|
|
@@ -814,6 +817,14 @@ function applyTagOverrides(dataset, tags) {
|
|
|
814
817
|
}
|
|
815
818
|
}
|
|
816
819
|
}
|
|
820
|
+
function syncMetaWithDataset(meta, dataset) {
|
|
821
|
+
if (typeof dataset.SOPInstanceUID === "string") {
|
|
822
|
+
meta.MediaStorageSOPInstanceUID = dataset.SOPInstanceUID;
|
|
823
|
+
}
|
|
824
|
+
if (typeof dataset.SOPClassUID === "string") {
|
|
825
|
+
meta.MediaStorageSOPClassUID = dataset.SOPClassUID;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
817
828
|
function buildMeta(uid, modality, transferSyntax = "explicit-vr-little-endian") {
|
|
818
829
|
return {
|
|
819
830
|
FileMetaInformationVersion: new Uint8Array([0, 1]).buffer,
|
|
@@ -900,6 +911,7 @@ function buildImageBuffer(spec, uid) {
|
|
|
900
911
|
}
|
|
901
912
|
applyTagOverrides(dataset, spec.tags);
|
|
902
913
|
const meta = buildMeta(effectiveUid, modality, spec.transferSyntax);
|
|
914
|
+
syncMetaWithDataset(meta, dataset);
|
|
903
915
|
applySizing(dataset, meta, spec);
|
|
904
916
|
return serializeDict(meta, dataset);
|
|
905
917
|
}
|
|
@@ -1020,6 +1032,7 @@ function writeNative(outPath, params, dims, zeroChunk) {
|
|
|
1020
1032
|
const meta = buildMeta(uid, modality);
|
|
1021
1033
|
const dataset = buildBaseImageDataset(uid, modality);
|
|
1022
1034
|
applyTagOverrides(dataset, tags);
|
|
1035
|
+
syncMetaWithDataset(meta, dataset);
|
|
1023
1036
|
dataset.Rows = dims.rows;
|
|
1024
1037
|
dataset.Columns = dims.columns;
|
|
1025
1038
|
const minimalPixelBytes = dataset.PixelData.byteLength;
|
|
@@ -1053,6 +1066,7 @@ function writeEncapsulated(outPath, params, pixelBytes, fragmentBytes, zeroChunk
|
|
|
1053
1066
|
};
|
|
1054
1067
|
const dataset = buildBaseImageDataset(uid, modality);
|
|
1055
1068
|
applyTagOverrides(dataset, tags);
|
|
1069
|
+
syncMetaWithDataset(meta, dataset);
|
|
1056
1070
|
dataset.PixelData = [new Uint8Array([]).buffer, new Uint8Array([0, 0]).buffer];
|
|
1057
1071
|
dataset._vrMap = { PixelData: "OB" };
|
|
1058
1072
|
const probe = serializeDict(meta, dataset);
|
|
@@ -1409,7 +1423,7 @@ function* planTree(nodes, scope, env) {
|
|
|
1409
1423
|
for (const node of nodes) {
|
|
1410
1424
|
if (isDirNode(node)) {
|
|
1411
1425
|
dirOrdinal++;
|
|
1412
|
-
const dirName = node.name ??
|
|
1426
|
+
const dirName = node.name ?? positionalName("dir", dirOrdinal);
|
|
1413
1427
|
const child = {
|
|
1414
1428
|
...scope,
|
|
1415
1429
|
dirs: [...scope.dirs, dirName],
|
|
@@ -1640,7 +1654,7 @@ import {
|
|
|
1640
1654
|
readSync,
|
|
1641
1655
|
statSync
|
|
1642
1656
|
} from "node:fs";
|
|
1643
|
-
import { join } from "node:path";
|
|
1657
|
+
import { basename, extname, join, relative, sep } from "node:path";
|
|
1644
1658
|
var dcmjsAny2 = dcmjs;
|
|
1645
1659
|
var HEX_TAG_RE2 = /^[0-9a-fA-F]{8}$/;
|
|
1646
1660
|
var SUPPORTED_MODALITIES = /* @__PURE__ */ new Set(["CT", "PT", "MR", "CR"]);
|
|
@@ -1904,6 +1918,37 @@ function mergeTags(a, b) {
|
|
|
1904
1918
|
if (!b) return a;
|
|
1905
1919
|
return { ...a, ...b };
|
|
1906
1920
|
}
|
|
1921
|
+
function scanFile(path) {
|
|
1922
|
+
let size;
|
|
1923
|
+
try {
|
|
1924
|
+
size = statSync(path).size;
|
|
1925
|
+
} catch {
|
|
1926
|
+
return null;
|
|
1927
|
+
}
|
|
1928
|
+
if (size > MAX_STREAM_BYTES) return null;
|
|
1929
|
+
const large = size > MAX_PIXEL_BYTES;
|
|
1930
|
+
return { size, large, record: large ? readHeaderOnly(path) : parseFull(path) };
|
|
1931
|
+
}
|
|
1932
|
+
function presetModalityOf(record) {
|
|
1933
|
+
if (typeof record.Modality !== "string") return void 0;
|
|
1934
|
+
return SUPPORTED_MODALITIES.has(record.Modality) ? record.Modality : "unsupported";
|
|
1935
|
+
}
|
|
1936
|
+
function hashRecordUids(rec, salt, uidMap, sweptKeepTags) {
|
|
1937
|
+
if (!rec.uidOriginals) return;
|
|
1938
|
+
if (rec.tags) {
|
|
1939
|
+
for (const keyword of Object.keys(rec.uidOriginals)) {
|
|
1940
|
+
if (keyword in rec.tags) sweptKeepTags.add(keyword);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
rec.tags = mergeTags(rec.tags, hashUidTags(rec.uidOriginals, salt, uidMap));
|
|
1944
|
+
}
|
|
1945
|
+
function warnSweptKeepTags(sweptKeepTags) {
|
|
1946
|
+
for (const keyword of sweptKeepTags) {
|
|
1947
|
+
console.warn(
|
|
1948
|
+
`describe: keep-tag "${keyword}" contains UID references \u2014 kept as its hashed UID skeleton, not verbatim (disable with preserveUids: false)`
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1907
1952
|
function sizeKbOf(bytes) {
|
|
1908
1953
|
return Math.max(1, Math.round(bytes / 1024));
|
|
1909
1954
|
}
|
|
@@ -2006,9 +2051,128 @@ function buildSeriesSpec(records) {
|
|
|
2006
2051
|
function seriesSpecFromAccum(accum) {
|
|
2007
2052
|
return Array.isArray(accum) ? buildSeriesSpec(accum) : { entries: [...accum.values()].map(entryOf) };
|
|
2008
2053
|
}
|
|
2054
|
+
function dirAccumFor(root, dirs) {
|
|
2055
|
+
let node = root;
|
|
2056
|
+
for (const name of dirs) {
|
|
2057
|
+
let child = node.dirs.get(name);
|
|
2058
|
+
if (!child) {
|
|
2059
|
+
child = { dirs: /* @__PURE__ */ new Map(), files: [] };
|
|
2060
|
+
node.dirs.set(name, child);
|
|
2061
|
+
}
|
|
2062
|
+
node = child;
|
|
2063
|
+
}
|
|
2064
|
+
return node;
|
|
2065
|
+
}
|
|
2066
|
+
var SAFE_EXTENSION_RE = /^\.[A-Za-z0-9]{1,8}$/;
|
|
2067
|
+
function safeExtension(path) {
|
|
2068
|
+
const ext = extname(basename(path));
|
|
2069
|
+
return SAFE_EXTENSION_RE.test(ext) ? ext : "";
|
|
2070
|
+
}
|
|
2071
|
+
function emitTree(acc) {
|
|
2072
|
+
const nodes = [];
|
|
2073
|
+
let otherOrdinal = 0;
|
|
2074
|
+
for (const leaf of acc.files) {
|
|
2075
|
+
if (leaf.kind === "other") {
|
|
2076
|
+
otherOrdinal++;
|
|
2077
|
+
const name = `${positionalName("file", otherOrdinal)}${leaf.ext}`;
|
|
2078
|
+
nodes.push(
|
|
2079
|
+
leaf.bytes > 0 ? { type: "non-dicom", name, targetBytes: leaf.bytes } : { type: "non-dicom", name, content: "" }
|
|
2080
|
+
);
|
|
2081
|
+
} else {
|
|
2082
|
+
nodes.push(entryOf({ ...leaf.rec, count: 1 }));
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
for (const sub of acc.dirs.values()) {
|
|
2086
|
+
nodes.push({ children: emitTree(sub) });
|
|
2087
|
+
}
|
|
2088
|
+
return nodes;
|
|
2089
|
+
}
|
|
2090
|
+
function describeTree(dir, options, keepKeywords) {
|
|
2091
|
+
const root = { dirs: /* @__PURE__ */ new Map(), files: [] };
|
|
2092
|
+
const stats = {
|
|
2093
|
+
dicomFiles: 0,
|
|
2094
|
+
skipped: 0,
|
|
2095
|
+
unknownModality: 0,
|
|
2096
|
+
nonDicomFiles: 0
|
|
2097
|
+
};
|
|
2098
|
+
const studyUids = /* @__PURE__ */ new Set();
|
|
2099
|
+
const dicomLeaves = [];
|
|
2100
|
+
for (const path of [...walkFiles(dir)].sort()) {
|
|
2101
|
+
const scanned = scanFile(path);
|
|
2102
|
+
if (!scanned) {
|
|
2103
|
+
stats.skipped++;
|
|
2104
|
+
continue;
|
|
2105
|
+
}
|
|
2106
|
+
const { size, large, record } = scanned;
|
|
2107
|
+
const studyUid = record?.StudyInstanceUID;
|
|
2108
|
+
const seriesUid = record?.SeriesInstanceUID;
|
|
2109
|
+
const relDirs = relative(dir, path).split(sep).slice(0, -1);
|
|
2110
|
+
if (!record || typeof studyUid !== "string" || typeof seriesUid !== "string") {
|
|
2111
|
+
if (large) {
|
|
2112
|
+
stats.skipped++;
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
stats.nonDicomFiles++;
|
|
2116
|
+
dirAccumFor(root, relDirs).files.push({
|
|
2117
|
+
kind: "other",
|
|
2118
|
+
bytes: size,
|
|
2119
|
+
ext: safeExtension(path)
|
|
2120
|
+
});
|
|
2121
|
+
continue;
|
|
2122
|
+
}
|
|
2123
|
+
stats.dicomFiles++;
|
|
2124
|
+
studyUids.add(studyUid);
|
|
2125
|
+
const preset = presetModalityOf(record);
|
|
2126
|
+
if (preset === "unsupported") stats.unknownModality++;
|
|
2127
|
+
const modality = preset === "unsupported" ? void 0 : preset;
|
|
2128
|
+
const instanceNumber = typeof record.InstanceNumber === "number" ? { InstanceNumber: record.InstanceNumber } : typeof record.InstanceNumber === "string" ? { InstanceNumber: escapeTagTemplate(record.InstanceNumber) } : void 0;
|
|
2129
|
+
const leaf = {
|
|
2130
|
+
kind: "dicom",
|
|
2131
|
+
rec: {
|
|
2132
|
+
large,
|
|
2133
|
+
bytes: size,
|
|
2134
|
+
modality,
|
|
2135
|
+
tags: mergeTags(
|
|
2136
|
+
instanceNumber,
|
|
2137
|
+
keepKeywords.length ? extractTags(record, keepKeywords) : void 0
|
|
2138
|
+
),
|
|
2139
|
+
uidOriginals: collectUidOriginals(record)
|
|
2140
|
+
},
|
|
2141
|
+
studyUid,
|
|
2142
|
+
seriesUid
|
|
2143
|
+
};
|
|
2144
|
+
dirAccumFor(root, relDirs).files.push(leaf);
|
|
2145
|
+
dicomLeaves.push(leaf);
|
|
2146
|
+
}
|
|
2147
|
+
if (root.files.length === 0 && root.dirs.size === 0) {
|
|
2148
|
+
throw new Error(`describe: no files found in ${dir}`);
|
|
2149
|
+
}
|
|
2150
|
+
const salt = options.uidSalt ?? deriveSalt([...studyUids]);
|
|
2151
|
+
const uidMap = /* @__PURE__ */ new Map();
|
|
2152
|
+
const sweptKeepTags = /* @__PURE__ */ new Set();
|
|
2153
|
+
for (const { rec, studyUid, seriesUid } of dicomLeaves) {
|
|
2154
|
+
hashRecordUids(rec, salt, uidMap, sweptKeepTags);
|
|
2155
|
+
rec.tags = mergeTags(rec.tags, {
|
|
2156
|
+
StudyInstanceUID: hashUidVia(uidMap, salt, studyUid),
|
|
2157
|
+
SeriesInstanceUID: hashUidVia(uidMap, salt, seriesUid)
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
warnSweptKeepTags(sweptKeepTags);
|
|
2161
|
+
const spec = { tree: emitTree(root), uidSalt: salt };
|
|
2162
|
+
validateDatasetSpec(spec);
|
|
2163
|
+
return { spec, stats };
|
|
2164
|
+
}
|
|
2009
2165
|
function describeDirectory(dir, options = {}) {
|
|
2010
2166
|
const keepKeywords = options.keepTags ? resolveKeepTags(options.keepTags) : [];
|
|
2011
2167
|
const preserveUids = options.preserveUids ?? true;
|
|
2168
|
+
if (options.captureTree) {
|
|
2169
|
+
if (!preserveUids) {
|
|
2170
|
+
throw new Error(
|
|
2171
|
+
"describe: captureTree requires preserveUids \u2014 the captured tree carries its grouping as hashed UID tags"
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
2174
|
+
return describeTree(dir, options, keepKeywords);
|
|
2175
|
+
}
|
|
2012
2176
|
const uidMap = /* @__PURE__ */ new Map();
|
|
2013
2177
|
const useRecords = keepKeywords.length > 0 || preserveUids;
|
|
2014
2178
|
const tolerance = options.sizeTolerance ?? 0;
|
|
@@ -2018,21 +2182,19 @@ function describeDirectory(dir, options = {}) {
|
|
|
2018
2182
|
);
|
|
2019
2183
|
}
|
|
2020
2184
|
const studies = /* @__PURE__ */ new Map();
|
|
2021
|
-
const stats = {
|
|
2185
|
+
const stats = {
|
|
2186
|
+
dicomFiles: 0,
|
|
2187
|
+
skipped: 0,
|
|
2188
|
+
unknownModality: 0,
|
|
2189
|
+
nonDicomFiles: 0
|
|
2190
|
+
};
|
|
2022
2191
|
for (const path of [...walkFiles(dir)].sort()) {
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
size = statSync(path).size;
|
|
2026
|
-
} catch {
|
|
2027
|
-
stats.skipped++;
|
|
2028
|
-
continue;
|
|
2029
|
-
}
|
|
2030
|
-
if (size > MAX_STREAM_BYTES) {
|
|
2192
|
+
const scanned = scanFile(path);
|
|
2193
|
+
if (!scanned) {
|
|
2031
2194
|
stats.skipped++;
|
|
2032
2195
|
continue;
|
|
2033
2196
|
}
|
|
2034
|
-
const large
|
|
2035
|
-
const record = large ? readHeaderOnly(path) : parseFull(path);
|
|
2197
|
+
const { size, large, record } = scanned;
|
|
2036
2198
|
const studyUid = record?.StudyInstanceUID;
|
|
2037
2199
|
const seriesUid = record?.SeriesInstanceUID;
|
|
2038
2200
|
if (!record || typeof studyUid !== "string" || typeof seriesUid !== "string") {
|
|
@@ -2040,14 +2202,9 @@ function describeDirectory(dir, options = {}) {
|
|
|
2040
2202
|
continue;
|
|
2041
2203
|
}
|
|
2042
2204
|
stats.dicomFiles++;
|
|
2043
|
-
|
|
2044
|
-
if (
|
|
2045
|
-
|
|
2046
|
-
modality = record.Modality;
|
|
2047
|
-
} else {
|
|
2048
|
-
stats.unknownModality++;
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2205
|
+
const preset = presetModalityOf(record);
|
|
2206
|
+
if (preset === "unsupported") stats.unknownModality++;
|
|
2207
|
+
const modality = preset === "unsupported" ? void 0 : preset;
|
|
2051
2208
|
const keptTags = keepKeywords.length ? extractTags(record, keepKeywords) : void 0;
|
|
2052
2209
|
const uidOriginals = preserveUids ? collectUidOriginals(record) : void 0;
|
|
2053
2210
|
const bytes = large ? size : Math.min(bucketBytes(size, tolerance), MAX_PIXEL_BYTES);
|
|
@@ -2081,25 +2238,11 @@ function describeDirectory(dir, options = {}) {
|
|
|
2081
2238
|
for (const series of study.values()) {
|
|
2082
2239
|
if (!Array.isArray(series)) continue;
|
|
2083
2240
|
for (const rec of series) {
|
|
2084
|
-
|
|
2085
|
-
if (rec.tags) {
|
|
2086
|
-
for (const keyword of Object.keys(rec.uidOriginals)) {
|
|
2087
|
-
if (keyword in rec.tags) sweptKeepTags.add(keyword);
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2090
|
-
rec.tags = mergeTags(
|
|
2091
|
-
rec.tags,
|
|
2092
|
-
hashUidTags(rec.uidOriginals, salt, uidMap)
|
|
2093
|
-
);
|
|
2094
|
-
}
|
|
2241
|
+
hashRecordUids(rec, salt, uidMap, sweptKeepTags);
|
|
2095
2242
|
}
|
|
2096
2243
|
}
|
|
2097
2244
|
}
|
|
2098
|
-
|
|
2099
|
-
console.warn(
|
|
2100
|
-
`describe: keep-tag "${keyword}" contains UID references \u2014 kept as its hashed UID skeleton, not verbatim (disable with preserveUids: false)`
|
|
2101
|
-
);
|
|
2102
|
-
}
|
|
2245
|
+
warnSweptKeepTags(sweptKeepTags);
|
|
2103
2246
|
}
|
|
2104
2247
|
const withGroupUid = (spec2, seriesUid) => salt === void 0 ? spec2 : {
|
|
2105
2248
|
...spec2,
|
|
@@ -407,6 +407,9 @@ function validateStudy(study, path) {
|
|
|
407
407
|
if ("tags" in st) validateTags(st.tags, `${path}.tags`);
|
|
408
408
|
return st;
|
|
409
409
|
}
|
|
410
|
+
function positionalName(prefix, ordinal) {
|
|
411
|
+
return `${prefix}-${String(ordinal).padStart(3, "0")}`;
|
|
412
|
+
}
|
|
410
413
|
var VALID_TREE_ROLES = {
|
|
411
414
|
study: true,
|
|
412
415
|
series: true
|
|
@@ -487,7 +490,7 @@ function validateTreeChildren(children, basePath, inStudy, inSeries) {
|
|
|
487
490
|
let name = "name" in n ? n.name : void 0;
|
|
488
491
|
if ("children" in n) {
|
|
489
492
|
dirOrdinal++;
|
|
490
|
-
name ?? (name =
|
|
493
|
+
name ?? (name = positionalName("dir", dirOrdinal));
|
|
491
494
|
}
|
|
492
495
|
if (name === void 0) return;
|
|
493
496
|
const prior = claimed.get(name);
|
|
@@ -679,6 +682,7 @@ function validateParametricSpec(raw) {
|
|
|
679
682
|
export {
|
|
680
683
|
HEX_TAG_RE,
|
|
681
684
|
isImageType,
|
|
685
|
+
positionalName,
|
|
682
686
|
validateDatasetSpec,
|
|
683
687
|
validateParametricSpec
|
|
684
688
|
};
|
|
@@ -108,6 +108,14 @@ function applyTagOverrides(dataset, tags) {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
+
function syncMetaWithDataset(meta, dataset) {
|
|
112
|
+
if (typeof dataset.SOPInstanceUID === "string") {
|
|
113
|
+
meta.MediaStorageSOPInstanceUID = dataset.SOPInstanceUID;
|
|
114
|
+
}
|
|
115
|
+
if (typeof dataset.SOPClassUID === "string") {
|
|
116
|
+
meta.MediaStorageSOPClassUID = dataset.SOPClassUID;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
111
119
|
function buildMeta(uid, modality, transferSyntax = "explicit-vr-little-endian") {
|
|
112
120
|
return {
|
|
113
121
|
FileMetaInformationVersion: new Uint8Array([0, 1]).buffer,
|
|
@@ -194,6 +202,7 @@ function buildImageBuffer(spec, uid) {
|
|
|
194
202
|
}
|
|
195
203
|
applyTagOverrides(dataset, spec.tags);
|
|
196
204
|
const meta = buildMeta(effectiveUid, modality, spec.transferSyntax);
|
|
205
|
+
syncMetaWithDataset(meta, dataset);
|
|
197
206
|
applySizing(dataset, meta, spec);
|
|
198
207
|
return serializeDict(meta, dataset);
|
|
199
208
|
}
|
|
@@ -234,5 +243,6 @@ export {
|
|
|
234
243
|
buildBufferForSpec,
|
|
235
244
|
buildMeta,
|
|
236
245
|
serializeDict,
|
|
237
|
-
sizedNonDicomBytes
|
|
246
|
+
sizedNonDicomBytes,
|
|
247
|
+
syncMetaWithDataset
|
|
238
248
|
};
|
|
@@ -94,6 +94,14 @@ function applyTagOverrides(dataset, tags) {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
+
function syncMetaWithDataset(meta, dataset) {
|
|
98
|
+
if (typeof dataset.SOPInstanceUID === "string") {
|
|
99
|
+
meta.MediaStorageSOPInstanceUID = dataset.SOPInstanceUID;
|
|
100
|
+
}
|
|
101
|
+
if (typeof dataset.SOPClassUID === "string") {
|
|
102
|
+
meta.MediaStorageSOPClassUID = dataset.SOPClassUID;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
97
105
|
function buildMeta(uid, modality, transferSyntax = "explicit-vr-little-endian") {
|
|
98
106
|
return {
|
|
99
107
|
FileMetaInformationVersion: new Uint8Array([0, 1]).buffer,
|
|
@@ -204,6 +212,7 @@ function writeNative(outPath, params, dims, zeroChunk) {
|
|
|
204
212
|
const meta = buildMeta(uid, modality);
|
|
205
213
|
const dataset = buildBaseImageDataset(uid, modality);
|
|
206
214
|
applyTagOverrides(dataset, tags);
|
|
215
|
+
syncMetaWithDataset(meta, dataset);
|
|
207
216
|
dataset.Rows = dims.rows;
|
|
208
217
|
dataset.Columns = dims.columns;
|
|
209
218
|
const minimalPixelBytes = dataset.PixelData.byteLength;
|
|
@@ -237,6 +246,7 @@ function writeEncapsulated(outPath, params, pixelBytes, fragmentBytes, zeroChunk
|
|
|
237
246
|
};
|
|
238
247
|
const dataset = buildBaseImageDataset(uid, modality);
|
|
239
248
|
applyTagOverrides(dataset, tags);
|
|
249
|
+
syncMetaWithDataset(meta, dataset);
|
|
240
250
|
dataset.PixelData = [new Uint8Array([]).buffer, new Uint8Array([0, 0]).buffer];
|
|
241
251
|
dataset._vrMap = { PixelData: "OB" };
|
|
242
252
|
const probe = serializeDict(meta, dataset);
|
|
@@ -4,6 +4,7 @@ export type DescribeOptions = {
|
|
|
4
4
|
sizeTolerance?: number;
|
|
5
5
|
preserveUids?: boolean;
|
|
6
6
|
uidSalt?: string;
|
|
7
|
+
captureTree?: boolean;
|
|
7
8
|
};
|
|
8
9
|
export type DescribeResult = {
|
|
9
10
|
spec: DatasetSpec;
|
|
@@ -11,6 +12,7 @@ export type DescribeResult = {
|
|
|
11
12
|
dicomFiles: number;
|
|
12
13
|
skipped: number;
|
|
13
14
|
unknownModality: number;
|
|
15
|
+
nonDicomFiles: number;
|
|
14
16
|
};
|
|
15
17
|
};
|
|
16
18
|
export declare function readHeaderOnly(path: string): Record<string, unknown> | null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DatasetSpec, FileSpec, ParametricSpec } from './types.js';
|
|
2
2
|
export declare function isImageType(type: FileSpec['type']): boolean;
|
|
3
3
|
export declare const HEX_TAG_RE: RegExp;
|
|
4
|
+
export declare function positionalName(prefix: string, ordinal: number): string;
|
|
4
5
|
export declare function validateDatasetSpec(raw: unknown): DatasetSpec;
|
|
5
6
|
export declare function validateParametricSpec(raw: unknown): ParametricSpec;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DicomTagOverrides, FileSpec, Modality, NonDicomSpec, TransferSyntax } from '../schema/types.js';
|
|
2
2
|
import type { UidSet } from './uid.js';
|
|
3
3
|
export declare function applyTagOverrides(dataset: Record<string, unknown>, tags: DicomTagOverrides | undefined): void;
|
|
4
|
+
export declare function syncMetaWithDataset(meta: Record<string, unknown>, dataset: Record<string, unknown>): void;
|
|
4
5
|
export declare function buildMeta(uid: UidSet, modality: Modality, transferSyntax?: TransferSyntax): Record<string, unknown>;
|
|
5
6
|
export declare function buildBaseImageDataset(uid: UidSet, modality: Modality): Record<string, unknown>;
|
|
6
7
|
export declare function serializeDict(meta: Record<string, unknown>, dataset: Record<string, unknown>): Buffer;
|