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 +4 -4
- package/dist/esm/applyMappingsWorker.js +6 -1
- package/dist/esm/collectMappings.js +17 -9
- package/dist/esm/config/dicom/tagConversion.js +44 -0
- package/dist/esm/curateDict.js +67 -2
- package/dist/esm/curateOne.js +3 -3
- package/dist/esm/deidentifyPS315E.js +54 -3
- package/dist/esm/getParser.js +11 -1
- package/dist/esm/index.js +7 -4
- package/dist/esm/offsetDateTime.js +5 -3
- package/dist/esm/scanDirectoryWorker.js +25 -16
- package/dist/types/applyMappingsWorker.d.ts +8 -1
- package/dist/types/collectMappings.d.ts +1 -1
- package/dist/types/config/dicom/tagConversion.d.ts +12 -0
- package/dist/types/curateDict.d.ts +1 -1
- package/dist/types/curateOne.d.ts +7 -1
- package/dist/types/deidentifyPS315E.d.ts +3 -1
- package/dist/types/getParser.d.ts +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/scanDirectoryWorker.d.ts +8 -0
- package/dist/types/serializeMappingOptions.d.ts +1 -5
- package/dist/types/types.d.ts +4 -0
- package/dist/umd/dicom-curate.umd.js +371 -191
- package/dist/umd/dicom-curate.umd.js.map +1 -1
- package/dist/umd/dicom-curate.umd.min.js +2 -2
- package/dist/umd/dicom-curate.umd.min.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
83
|
-
|
|
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(
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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,
|
|
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
|
+
}
|
package/dist/esm/curateDict.js
CHANGED
|
@@ -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
|
}
|
package/dist/esm/curateOne.js
CHANGED
|
@@ -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(
|
|
14
|
-
return __awaiter(this,
|
|
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 =
|
|
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
|
-
|
|
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 {
|
package/dist/esm/getParser.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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,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 {};
|
package/dist/types/types.d.ts
CHANGED
|
@@ -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;
|