dicom-curate 0.3.0 → 0.4.1

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 CHANGED
@@ -77,11 +77,11 @@ const columnMappings = extractColumnMappings([
77
77
  { subjectID: 'SubjectID2', blindedID: 'BlindedID2' },
78
78
  ])
79
79
 
80
- curateOne(
80
+ curateOne({
81
81
  fileInfo, // path, name, size, kind, blob
82
- undefined,
83
- { curationSpec, columnMappings },
84
- )
82
+ mappingOptions: { curationSpec, columnMappings },
83
+ })
84
+
85
85
 
86
86
  // Cache clean-up responsibility, e.g. for consistent UID mapping in `retainUIDsOption: 'Off'` is with caller
87
87
  clearCaches()
@@ -6,7 +6,12 @@ self.addEventListener('message', (event) => {
6
6
  const { serializedMappingOptions } = event.data;
7
7
  const mappingOptions = deserializeMappingOptions(serializedMappingOptions);
8
8
  try {
9
- curateOne(event.data.fileInfo, event.data.outputDirectory, mappingOptions).then((mapResults) => {
9
+ curateOne({
10
+ fileInfo: event.data.fileInfo,
11
+ fileIndex: event.data.fileIndex,
12
+ outputDirectory: event.data.outputDirectory,
13
+ mappingOptions,
14
+ }).then((mapResults) => {
10
15
  // Send finished message for completion
11
16
  self.postMessage({
12
17
  response: 'finished',
@@ -14,7 +14,7 @@ import deidentifyPS315E, { defaultPs315Options } from './deidentifyPS315E.js';
14
14
  import getParser from './getParser.js';
15
15
  import { specVersion } from './config/specVersion.js';
16
16
  import { get as _get } from 'lodash';
17
- export default function collectMappings(inputFilePath, dicomData, mappingOptions) {
17
+ export default function collectMappings(inputFilePath, inputFileIndex, dicomData, mappingOptions) {
18
18
  var _a;
19
19
  const mapResults = {
20
20
  // original UID for this dicomData
@@ -33,13 +33,15 @@ export default function collectMappings(inputFilePath, dicomData, mappingOptions
33
33
  let finalSpec = {
34
34
  dicomPS315EOptions: defaultPs315Options,
35
35
  inputPathPattern: '',
36
- modifications: () => ({
37
- dicomHeader: {},
38
- outputFilePathComponents: [
39
- parser.getDicom('SeriesInstanceUID'),
40
- parser.getFilePathComp(parser.FILENAME),
41
- ],
42
- }),
36
+ modifications(parser) {
37
+ return {
38
+ dicomHeader: {},
39
+ outputFilePathComponents: [
40
+ parser.protectUid(parser.getDicom('SeriesInstanceUID')),
41
+ parser.getFilePathComp(parser.FILENAME),
42
+ ],
43
+ };
44
+ },
43
45
  validation: () => ({ errors: [] }),
44
46
  };
45
47
  const _b = mappingOptions.curationSpec(), { modifications, validation } = _b, restSpec = __rest(_b, ["modifications", "validation"]);
@@ -54,8 +56,13 @@ export default function collectMappings(inputFilePath, dicomData, mappingOptions
54
56
  if (!mappingOptions.skipValidation) {
55
57
  finalSpec.validation = validation;
56
58
  }
59
+ // protect filename if we de-identify
60
+ const finalFilePath = finalSpec.dicomPS315EOptions === 'Off'
61
+ ? inputFilePath
62
+ : inputFilePath.slice(0, inputFilePath.lastIndexOf('/') + 1) +
63
+ `${String(inputFileIndex + 1).padStart(5, '0')}.dcm`;
57
64
  // create a parser object to be used in the eval'ed mappingFunctions
58
- const parser = getParser(finalSpec.inputPathPattern, inputFilePath, naturalData, mappingOptions.columnMappings, finalSpec.additionalData);
65
+ const parser = getParser(finalSpec.inputPathPattern, finalFilePath, naturalData, finalSpec.dicomPS315EOptions, mappingOptions.columnMappings, finalSpec.additionalData);
59
66
  let modificationMap = finalSpec.modifications(parser);
60
67
  // List all validation errors
61
68
  mapResults.errors = finalSpec
@@ -92,6 +99,7 @@ export default function collectMappings(inputFilePath, dicomData, mappingOptions
92
99
  dicomPS315EOptions: finalSpec.dicomPS315EOptions,
93
100
  dateOffset: mappingOptions.dateOffset,
94
101
  mapResults,
102
+ originalDicomDict: dicomData.dict,
95
103
  });
96
104
  }
97
105
  // Moving this after collectMappingsInData as this should take precedence.
@@ -0,0 +1,44 @@
1
+ import * as dcmjs from 'dcmjs';
2
+ /**
3
+ * Check if a tag identifier is a private tag
4
+ */
5
+ export function isPrivateTag(tagId) {
6
+ // Check if it's already a tag ID format (8 hex digits)
7
+ if (/^[0-9A-Fa-f]{8}$/.test(tagId)) {
8
+ const group = parseInt(tagId.substring(0, 4), 16);
9
+ return group % 2 === 1;
10
+ }
11
+ // If it's a keyword, it's not a private tag
12
+ return false;
13
+ }
14
+ /**
15
+ * Convert a DICOM keyword to its corresponding tag ID
16
+ */
17
+ export function convertKeywordToTagId(keyword) {
18
+ var _a;
19
+ // Use dcmjs built-in conversion for standard DICOM keywords
20
+ // For private tags (which don't have keywords), keep as-is
21
+ const tagId = isPrivateTag(keyword) ? keyword :
22
+ ((_a = dcmjs.data.DicomMetaDictionary.nameMap[keyword]) === null || _a === void 0 ? void 0 : _a.tag) || keyword;
23
+ // Remove parentheses and commas, convert to the format used in dictionary keys
24
+ return tagId.replace(/[(),]/g, '').toLowerCase();
25
+ }
26
+ /**
27
+ * Convert a keyword path to tag ID path for nested DICOM elements
28
+ */
29
+ export function convertKeywordPathToTagIdPath(keywordPath) {
30
+ // Handle nested paths like "GeneralMatchingSequence[0].00510014"
31
+ const parts = keywordPath.split('.');
32
+ const convertedParts = parts.map(part => {
33
+ const arrayMatch = part.match(/^(.+)\[(\d+)\]$/);
34
+ if (arrayMatch) {
35
+ const [, keyword, index] = arrayMatch;
36
+ const tagId = convertKeywordToTagId(keyword);
37
+ return `${tagId}[${index}]`;
38
+ }
39
+ else {
40
+ return convertKeywordToTagId(part);
41
+ }
42
+ });
43
+ return convertedParts.join('.');
44
+ }
@@ -1,12 +1,13 @@
1
1
  import * as dcmjs from 'dcmjs';
2
2
  import collectMappings from './collectMappings.js';
3
3
  import mapMetaheader from './mapMetaheader.js';
4
+ import { convertKeywordPathToTagIdPath } from './config/dicom/tagConversion.js';
4
5
  import { set as _set, unset as _unset, cloneDeep as _cloneDeep } from 'lodash';
5
- export default function curateDict(inputFilePath, dicomData, mappingOptions) {
6
+ export default function curateDict(inputFilePath, inputFileIndex, dicomData, mappingOptions) {
6
7
  //
7
8
  // Collect the mappings and apply them to the data
8
9
  //
9
- const [naturalData, mapResults] = collectMappings(inputFilePath, dicomData, mappingOptions);
10
+ const [naturalData, mapResults] = collectMappings(inputFilePath, inputFileIndex, dicomData, mappingOptions);
10
11
  for (let tagPath in mapResults.mappings) {
11
12
  const [, operation, , mappedValue] = mapResults.mappings[tagPath];
12
13
  switch (operation) {
@@ -27,5 +28,69 @@ export default function curateDict(inputFilePath, dicomData, mappingOptions) {
27
28
  mapMetaheader(dicomData.meta, naturalData.SOPInstanceUID));
28
29
  mappedDicomData.dict =
29
30
  dcmjs.data.DicomMetaDictionary.denaturalizeDataset(naturalData);
31
+ // Restore quarantined private tags directly to the final DICOM dict
32
+ // This must be done after denaturalization since private tags aren't in the dictionary
33
+ for (let tagPath in mapResults.quarantine) {
34
+ const quarantinedElement = mapResults.quarantine[tagPath];
35
+ if (!quarantinedElement)
36
+ continue;
37
+ // Convert keyword paths to tag ID paths for restoration
38
+ const tagIdPath = convertKeywordPathToTagIdPath(tagPath);
39
+ // If the quarantined element has DICOM structure (vr and Value), restore it directly
40
+ if (quarantinedElement && typeof quarantinedElement === 'object' && 'Value' in quarantinedElement) {
41
+ // Handle nested paths like "00080413[0].00510014"
42
+ const pathParts = tagIdPath.split('.');
43
+ if (pathParts.length === 2 && pathParts[0].includes('[')) {
44
+ // This is a nested path, handle it specially
45
+ const [sequenceWithIndex, privateTagId] = pathParts;
46
+ const arrayMatch = sequenceWithIndex.match(/^(.+)\[(\d+)\]$/);
47
+ if (arrayMatch) {
48
+ const [, sequenceTagId, index] = arrayMatch;
49
+ let sequence = mappedDicomData.dict[sequenceTagId];
50
+ // If the sequence doesn't exist, we need to create it
51
+ if (!sequence) {
52
+ // Create the sequence with the private tag already included
53
+ const sequenceItemWithPrivateTag = { [privateTagId]: quarantinedElement };
54
+ sequence = {
55
+ vr: 'SQ',
56
+ Value: [sequenceItemWithPrivateTag]
57
+ };
58
+ mappedDicomData.dict[sequenceTagId] = sequence;
59
+ }
60
+ else {
61
+ // Ensure the sequence has a Value array
62
+ if (!sequence.Value) {
63
+ sequence.Value = [];
64
+ }
65
+ // Ensure we have enough items in the sequence
66
+ while (sequence.Value.length <= parseInt(index)) {
67
+ sequence.Value.push({});
68
+ }
69
+ if (sequence && sequence.Value && sequence.Value[parseInt(index)]) {
70
+ // Ensure the sequence item is properly structured
71
+ const sequenceItem = sequence.Value[parseInt(index)];
72
+ if (typeof sequenceItem === 'object' && sequenceItem !== null) {
73
+ // Create a new object with the private tag included
74
+ const newSequenceItem = Object.assign(Object.assign({}, sequenceItem), { [privateTagId]: quarantinedElement });
75
+ sequence.Value[parseInt(index)] = newSequenceItem;
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+ else {
82
+ // Top-level private tag
83
+ _set(mappedDicomData.dict, tagIdPath, quarantinedElement);
84
+ }
85
+ }
86
+ else {
87
+ // For raw values, we need to create a proper DICOM element structure
88
+ // This is a fallback - ideally all quarantined elements should have proper structure
89
+ _set(mappedDicomData.dict, tagIdPath, {
90
+ vr: 'UN', // Unknown VR for private tags
91
+ Value: Array.isArray(quarantinedElement) ? quarantinedElement : [quarantinedElement]
92
+ });
93
+ }
94
+ }
30
95
  return { dicomData: mappedDicomData, mapResults: _cloneDeep(mapResults) };
31
96
  }
@@ -10,8 +10,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import * as dcmjs from 'dcmjs';
11
11
  import createNestedDirectories from './createNestedDirectories.js';
12
12
  import curateDict from './curateDict.js';
13
- export function curateOne(fileInfo, outputDirectory, mappingOptions) {
14
- return __awaiter(this, void 0, void 0, function* () {
13
+ export function curateOne(_a) {
14
+ return __awaiter(this, arguments, void 0, function* ({ fileInfo, fileIndex = 0, outputDirectory, mappingOptions, }) {
15
15
  //
16
16
  // First, read the dicom instance data from the file handle or blob
17
17
  //
@@ -36,7 +36,7 @@ export function curateOne(fileInfo, outputDirectory, mappingOptions) {
36
36
  };
37
37
  return mapResults;
38
38
  }
39
- const { dicomData: mappedDicomData, mapResults: clonedMapResults } = curateDict(`${fileInfo.path}/${fileInfo.name}`, dicomData, mappingOptions);
39
+ const { dicomData: mappedDicomData, mapResults: clonedMapResults } = curateDict(`${fileInfo.path}/${fileInfo.name}`, fileIndex, dicomData, mappingOptions);
40
40
  if (!mappingOptions.skipWrite) {
41
41
  // Finally, write the results
42
42
  const dirPath = clonedMapResults.outputFilePath
@@ -15,6 +15,7 @@ import hashUid from './hashUid.js';
15
15
  import replaceUid from './replaceUid.js';
16
16
  import { elementNamesToAlwaysKeep } from './config/dicom/elementNamesToAlwaysKeep.js';
17
17
  import { ps315EElements as rawPs315EElements } from './config/dicom/ps315EElements.js';
18
+ import { convertKeywordToTagId } from './config/dicom/tagConversion.js';
18
19
  import { offsetDateTime } from './offsetDateTime.js';
19
20
  import { retainAdditionalIds } from './config/dicom/retainAdditionalIds.js';
20
21
  import { uidRegistryPS3_06_A1 } from './config/dicom/uidRegistryPS3_06_A1.js';
@@ -33,6 +34,9 @@ function temporalVr(vr) {
33
34
  function removeRetiredPrefix(name) {
34
35
  return name.startsWith('RETIRED_') ? name.slice(8) : name;
35
36
  }
37
+ export function protectUid(uid, retainUIDsOption) {
38
+ return retainUIDsOption === 'Hashed' ? hashUid(uid) : replaceUid(uid);
39
+ }
36
40
  const elementNamesToAlwaysKeepSet = new Set(elementNamesToAlwaysKeep);
37
41
  // Special conditions for some PS3.15 E1.1 elements.
38
42
  const ps315EElements = rawPs315EElements.map((elm) => {
@@ -52,7 +56,44 @@ const ps315EElements = rawPs315EElements.map((elm) => {
52
56
  return elm;
53
57
  }
54
58
  });
55
- export default function deidentifyPS315E({ naturalData, dicomPS315EOptions, dateOffset, mapResults, }) {
59
+ export default function deidentifyPS315E({ naturalData, dicomPS315EOptions, dateOffset, mapResults, originalDicomDict, }) {
60
+ // Helper function to get original DICOM element from nested path
61
+ function getOriginalDicomElement(path, tagName) {
62
+ if (!originalDicomDict)
63
+ return null;
64
+ if (!path) {
65
+ // Top-level element
66
+ return originalDicomDict[tagName];
67
+ }
68
+ // Parse nested path like "GeneralMatchingSequence[0]."
69
+ const pathParts = path.split('.');
70
+ let current = originalDicomDict;
71
+ for (const part of pathParts) {
72
+ if (!part)
73
+ continue; // Skip empty parts from trailing dots
74
+ const arrayMatch = part.match(/^(.+)\[(\d+)\]$/);
75
+ if (arrayMatch) {
76
+ const [, sequenceName, index] = arrayMatch;
77
+ const tagId = convertKeywordToTagId(sequenceName);
78
+ if (current[tagId] && current[tagId].Value && current[tagId].Value[parseInt(index)]) {
79
+ current = current[tagId].Value[parseInt(index)];
80
+ }
81
+ else {
82
+ return null;
83
+ }
84
+ }
85
+ else {
86
+ const tagId = convertKeywordToTagId(part);
87
+ if (current[tagId]) {
88
+ current = current[tagId];
89
+ }
90
+ else {
91
+ return null;
92
+ }
93
+ }
94
+ }
95
+ return current[tagName] || null;
96
+ }
56
97
  const { cleanDescriptorsOption, cleanDescriptorsExceptions, retainLongitudinalTemporalInformationOptions, retainPatientCharacteristicsOption, retainDeviceIdentityOption, retainUIDsOption, retainSafePrivateOption, retainInstitutionIdentityOption, } = dicomPS315EOptions;
57
98
  const taggedps315EEls = ps315EElements.reduce((acc, item) => {
58
99
  acc.push(item);
@@ -235,7 +276,7 @@ export default function deidentifyPS315E({ naturalData, dicomPS315EOptions, date
235
276
  // UID is not a known class UID.
236
277
  !(uid in uidRegistryPS3_06_A1)) {
237
278
  // UIDs that need to be mapped
238
- const mappedUID = retainUIDsOption === 'Hashed' ? hashUid(uid) : replaceUid(uid);
279
+ const mappedUID = protectUid(uid, retainUIDsOption);
239
280
  mapResults.mappings[attrPath] = [
240
281
  uid,
241
282
  'replace',
@@ -324,7 +365,17 @@ export default function deidentifyPS315E({ naturalData, dicomPS315EOptions, date
324
365
  }
325
366
  else {
326
367
  // We keep the private tag but register its value for checking.
327
- mapResults.quarantine[attrPath] = data[name];
368
+ // Store the full DICOM element structure, not just the value
369
+ // For private tags, we need to preserve the original structure
370
+ const originalElement = getOriginalDicomElement(path, name);
371
+ if (originalElement) {
372
+ // Store the original DICOM element structure (with vr and Value)
373
+ mapResults.quarantine[attrPath] = originalElement;
374
+ }
375
+ else {
376
+ // Fallback to the naturalized value if original not available
377
+ mapResults.quarantine[attrPath] = data[name];
378
+ }
328
379
  }
329
380
  }
330
381
  else {
@@ -1,4 +1,5 @@
1
1
  import * as dcmjs from 'dcmjs';
2
+ import { protectUid as rawProtectUid } from './deidentifyPS315E.js';
2
3
  import { getCsvMapping } from './csvMapping.js';
3
4
  import { UniqueNumbers } from './UniqueNumbers.js';
4
5
  export const FILEBASENAME = Symbol('fileBasename');
@@ -25,7 +26,15 @@ const { isUniqueInGroup, clearUniqueInGroupCache } = (function () {
25
26
  };
26
27
  })();
27
28
  export { clearUniqueNumberCache, clearUniqueInGroupCache };
28
- export default function getParser(inputPathPattern, inputFilePath, naturalData, columnMappings, additionalData) {
29
+ export default function getParser(inputPathPattern, inputFilePath, naturalData, dicomPS315EOptions, columnMappings, additionalData) {
30
+ function protectUid(uid) {
31
+ let protectedUid = uid;
32
+ if (dicomPS315EOptions !== 'Off') {
33
+ const { retainUIDsOption } = dicomPS315EOptions;
34
+ protectedUid = rawProtectUid(uid, retainUIDsOption);
35
+ }
36
+ return protectedUid;
37
+ }
29
38
  function getDicom(attrName) {
30
39
  if (attrName in dcmjs.data.DicomMetaDictionary.dictionary) {
31
40
  // if in hex like "(0008,0100)", convert to text key
@@ -92,6 +101,7 @@ export default function getParser(inputPathPattern, inputFilePath, naturalData,
92
101
  getMapping,
93
102
  getDicom,
94
103
  missingDicom,
104
+ protectUid,
95
105
  // TODO: Phase this out in favor of ISO8601 duration handling.
96
106
  // Example of this logic:
97
107
  // ContentDate:
package/dist/esm/index.js CHANGED
@@ -51,7 +51,8 @@ function initializeFileListWorker() {
51
51
  fileListWorker.addEventListener('message', (event) => {
52
52
  switch (event.data.response) {
53
53
  case 'file':
54
- filesToProcess.push(event.data.fileInfo);
54
+ const { fileIndex, fileInfo } = event.data;
55
+ filesToProcess.push({ fileIndex, fileInfo });
55
56
  // Could do some throttling:
56
57
  // if (filesToProcess.length > 10) {
57
58
  // fileListWorker.postMessage({ request: 'stop' })
@@ -63,6 +64,7 @@ function initializeFileListWorker() {
63
64
  directoryScanFinished = true;
64
65
  break;
65
66
  default:
67
+ // @ts-expect-error: response is string here, not never
66
68
  console.error(`Unknown response from worker ${event.data.response}`);
67
69
  }
68
70
  dispatchMappingJobs();
@@ -117,7 +119,7 @@ function initializeMappingWorkers() {
117
119
  }
118
120
  function dispatchMappingJobs() {
119
121
  while (filesToProcess.length > 0 && availableMappingWorkers.length > 0) {
120
- const fileInfo = filesToProcess.pop();
122
+ const { fileInfo, fileIndex } = filesToProcess.pop();
121
123
  const mappingWorker = availableMappingWorkers.pop();
122
124
  const _a =
123
125
  // Not partial anymore.
@@ -125,6 +127,7 @@ function dispatchMappingJobs() {
125
127
  mappingWorker.postMessage({
126
128
  request: 'apply',
127
129
  fileInfo,
130
+ fileIndex,
128
131
  outputDirectory,
129
132
  serializedMappingOptions: serializeMappingOptions(mappingOptions),
130
133
  });
@@ -186,7 +189,7 @@ function collectMappingOptions(organizeOptions) {
186
189
  });
187
190
  }
188
191
  function queueFilesForMapping(organizeOptions) {
189
- organizeOptions.inputFiles.forEach((inputFile) => {
192
+ organizeOptions.inputFiles.forEach((inputFile, fileIndex) => {
190
193
  const fileInfo = {
191
194
  path: '',
192
195
  name: inputFile.name,
@@ -194,7 +197,7 @@ function queueFilesForMapping(organizeOptions) {
194
197
  kind: 'blob',
195
198
  blob: inputFile,
196
199
  };
197
- filesToProcess.push(fileInfo);
200
+ filesToProcess.push({ fileInfo, fileIndex });
198
201
  dispatchMappingJobs();
199
202
  });
200
203
  }
@@ -197,7 +197,9 @@ function getDurationFractionMicroseconds(iso8601Duration) {
197
197
  * @returns The offset DICOM string, formatted like the original.
198
198
  */
199
199
  export function offsetDateTime(dicomValue, iso8601Duration) {
200
- // Step 0: Detect and handle a leading minus sign.
200
+ // Step 0: Trim leading/trailing spaces from the DICOM value
201
+ const trimmedDicomValue = dicomValue.trim();
202
+ // Step 0.5: Detect and handle a leading minus sign.
201
203
  let sign = 1;
202
204
  let durationStr = iso8601Duration;
203
205
  if (iso8601Duration.startsWith('-')) {
@@ -205,7 +207,7 @@ export function offsetDateTime(dicomValue, iso8601Duration) {
205
207
  durationStr = iso8601Duration.slice(1);
206
208
  }
207
209
  // Step 1: Convert the original DICOM string to a canonical DT string.
208
- const canonical = dicomToCanonicalDT(dicomValue); // Format: "YYYYMMDDHHMMSS.FFFFFF"
210
+ const canonical = dicomToCanonicalDT(trimmedDicomValue); // Format: "YYYYMMDDHHMMSS.FFFFFF"
209
211
  // Step 2: Split the canonical DT string.
210
212
  const base = canonical.slice(0, 14); // 14-digit base: YYYYMMDDHHMMSS
211
213
  const fractionStr = canonical.slice(15, 21); // 6-digit fractional part: FFFFFF
@@ -238,5 +240,5 @@ export function offsetDateTime(dicomValue, iso8601Duration) {
238
240
  const newFractionStr = newFractionMicro.toString().padStart(6, '0');
239
241
  const newCanonical = newBase + '.' + newFractionStr;
240
242
  // Step 9: Convert the canonical DT back to the original DICOM format.
241
- return canonicalDTToDicom(newCanonical, dicomValue);
243
+ return canonicalDTToDicom(newCanonical, trimmedDicomValue);
242
244
  }
@@ -34,27 +34,14 @@ function scanDirectory(dir) {
34
34
  function traverse(dir, prefix) {
35
35
  return __awaiter(this, void 0, void 0, function* () {
36
36
  var _a, e_1, _b, _c;
37
+ // First, collect sorted dir entries
38
+ const entries = [];
37
39
  try {
38
40
  for (var _d = true, _e = __asyncValues(dir.values()), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
39
41
  _c = _f.value;
40
42
  _d = false;
41
43
  const entry = _c;
42
- if (entry.kind === 'file' && keepScanning) {
43
- const file = yield entry.getFile();
44
- self.postMessage({
45
- response: 'file',
46
- fileInfo: {
47
- path: prefix,
48
- name: entry.name,
49
- size: file.size,
50
- kind: 'handle',
51
- fileHandle: entry,
52
- },
53
- });
54
- }
55
- else if (entry.kind === 'directory' && keepScanning) {
56
- yield traverse(entry, prefix + '/' + entry.name);
57
- }
44
+ entries.push(entry);
58
45
  }
59
46
  }
60
47
  catch (e_1_1) { e_1 = { error: e_1_1 }; }
@@ -64,6 +51,28 @@ function scanDirectory(dir) {
64
51
  }
65
52
  finally { if (e_1) throw e_1.error; }
66
53
  }
54
+ entries.sort((a, b) => a.name.localeCompare(b.name));
55
+ // Assign sorted index to files
56
+ let fileIndex = 0;
57
+ for (const entry of entries) {
58
+ if (entry.kind === 'file' && keepScanning) {
59
+ const file = yield entry.getFile();
60
+ self.postMessage({
61
+ response: 'file',
62
+ fileIndex: fileIndex++,
63
+ fileInfo: {
64
+ path: prefix,
65
+ name: entry.name,
66
+ size: file.size,
67
+ kind: 'handle',
68
+ fileHandle: entry,
69
+ },
70
+ });
71
+ }
72
+ else if (entry.kind === 'directory' && keepScanning) {
73
+ yield traverse(entry, prefix + '/' + entry.name);
74
+ }
75
+ }
67
76
  });
68
77
  }
69
78
  yield traverse(dir, dir.name);
@@ -1 +1,8 @@
1
- export {};
1
+ import type { TFileInfo, TSerializedMappingOptions } from './types';
2
+ export type MappingRequest = {
3
+ request: 'apply';
4
+ fileInfo: TFileInfo;
5
+ fileIndex: number;
6
+ outputDirectory?: FileSystemDirectoryHandle;
7
+ serializedMappingOptions: TSerializedMappingOptions;
8
+ };
@@ -1,3 +1,3 @@
1
1
  import type { TMappingOptions, TMapResults } from './types';
2
2
  import type { TDicomData, TNaturalData } from 'dcmjs';
3
- export default function collectMappings(inputFilePath: string, dicomData: TDicomData, mappingOptions: TMappingOptions): [TNaturalData, TMapResults];
3
+ export default function collectMappings(inputFilePath: string, inputFileIndex: number, dicomData: TDicomData, mappingOptions: TMappingOptions): [TNaturalData, TMapResults];
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Check if a tag identifier is a private tag
3
+ */
4
+ export declare function isPrivateTag(tagId: string): boolean;
5
+ /**
6
+ * Convert a DICOM keyword to its corresponding tag ID
7
+ */
8
+ export declare function convertKeywordToTagId(keyword: string): string;
9
+ /**
10
+ * Convert a keyword path to tag ID path for nested DICOM elements
11
+ */
12
+ export declare function convertKeywordPathToTagIdPath(keywordPath: string): string;
@@ -1,7 +1,7 @@
1
1
  import * as dcmjs from 'dcmjs';
2
2
  import type { TDicomData } from 'dcmjs';
3
3
  import type { TMappingOptions } from './types';
4
- export default function curateDict(inputFilePath: string, dicomData: TDicomData, mappingOptions: TMappingOptions): {
4
+ export default function curateDict(inputFilePath: string, inputFileIndex: number, dicomData: TDicomData, mappingOptions: TMappingOptions): {
5
5
  dicomData: dcmjs.data.DicomDict;
6
6
  mapResults: import("./types").TMapResults;
7
7
  };
@@ -1,4 +1,10 @@
1
1
  import type { TFileInfo, TMappingOptions, TMapResults } from './types';
2
- export declare function curateOne(fileInfo: TFileInfo, outputDirectory: FileSystemDirectoryHandle | undefined, mappingOptions: TMappingOptions): Promise<Omit<Partial<TMapResults>, 'anomalies'> & {
2
+ export type TCurateOneArgs = {
3
+ fileInfo: TFileInfo;
4
+ fileIndex?: number;
5
+ outputDirectory: FileSystemDirectoryHandle | undefined;
6
+ mappingOptions: TMappingOptions;
7
+ };
8
+ export declare function curateOne({ fileInfo, fileIndex, outputDirectory, mappingOptions, }: TCurateOneArgs): Promise<Omit<Partial<TMapResults>, 'anomalies'> & {
3
9
  anomalies: TMapResults['anomalies'];
4
10
  }>;
@@ -1,9 +1,11 @@
1
1
  import type { TNaturalData } from 'dcmjs';
2
2
  import type { Iso8601Duration, TPs315Options, TMapResults } from './types';
3
- export default function deidentifyPS315E({ naturalData, dicomPS315EOptions, dateOffset, mapResults, }: {
3
+ export declare function protectUid(uid: string, retainUIDsOption: string): string;
4
+ export default function deidentifyPS315E({ naturalData, dicomPS315EOptions, dateOffset, mapResults, originalDicomDict, }: {
4
5
  naturalData: TNaturalData;
5
6
  dicomPS315EOptions: TPs315Options;
6
7
  dateOffset?: Iso8601Duration;
7
8
  mapResults: TMapResults;
9
+ originalDicomDict?: Record<string, any>;
8
10
  }): void;
9
11
  export declare const defaultPs315Options: TPs315Options;
@@ -6,4 +6,4 @@ export declare const FILENAME: symbol;
6
6
  declare const clearUniqueNumberCache: () => void;
7
7
  declare const clearUniqueInGroupCache: () => void;
8
8
  export { clearUniqueNumberCache, clearUniqueInGroupCache };
9
- export default function getParser(inputPathPattern: string, inputFilePath: string, naturalData: TNaturalData, columnMappings?: TColumnMappings, additionalData?: TCurationSpecification['additionalData']): TParser;
9
+ export default function getParser(inputPathPattern: string, inputFilePath: string, naturalData: TNaturalData, dicomPS315EOptions: TCurationSpecification['dicomPS315EOptions'], columnMappings?: TColumnMappings, additionalData?: TCurationSpecification['additionalData']): TParser;
@@ -4,6 +4,7 @@ import { curateOne } from './curateOne';
4
4
  import type { OrganizeOptions, TProgressMessage, TProgressMessageDone } from './types';
5
5
  export type ProgressCallback = (message: TProgressMessage) => void;
6
6
  export type { TPs315Options, TMapResults, TProgressMessage, OrganizeOptions, TCurationSpecification, } from './types';
7
+ export { TCurateOneArgs } from './curateOne';
7
8
  export { specVersion } from './config/specVersion';
8
9
  export { sample2PassCurationSpecification as sampleSpecification } from './config/sample2PassCurationSpecification';
9
10
  export { csvTextToRows } from './csvMapping';
@@ -1 +1,9 @@
1
+ import type { TFileInfo } from './types';
1
2
  export {};
3
+ export type FileScanMsg = {
4
+ response: 'file';
5
+ fileIndex: number;
6
+ fileInfo: TFileInfo;
7
+ } | {
8
+ response: 'done';
9
+ };
@@ -1,7 +1,3 @@
1
- import { TMappingOptions } from './types';
2
- type TSerializedMappingOptions = Omit<TMappingOptions, 'curationSpec'> & {
3
- curationSpecStr: string;
4
- };
1
+ import { TMappingOptions, TSerializedMappingOptions } from './types';
5
2
  export declare function serializeMappingOptions(mappingOptions: TMappingOptions): TSerializedMappingOptions;
6
3
  export declare function deserializeMappingOptions(serializedMappingOptions: TSerializedMappingOptions): TMappingOptions;
7
- export {};
@@ -34,6 +34,9 @@ export type TMappingOptions = {
34
34
  skipValidation?: boolean;
35
35
  dateOffset?: Iso8601Duration;
36
36
  };
37
+ export type TSerializedMappingOptions = Omit<TMappingOptions, 'curationSpec'> & {
38
+ curationSpecStr: string;
39
+ };
37
40
  export type TFileInfo = {
38
41
  path: string;
39
42
  name: string;
@@ -92,6 +95,7 @@ export type TParser = {
92
95
  getMapping: ((value: string) => string | number) | undefined;
93
96
  getDicom: (attrName: string) => any;
94
97
  missingDicom: (attrName: string) => boolean;
98
+ protectUid: (uid: string) => string;
95
99
  addDays: (dicomDateString: string, offsetDays: number) => string;
96
100
  FILENAME: symbol;
97
101
  FILEBASENAME: symbol;