dicom-curate 0.40.7 → 0.41.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.
@@ -53787,7 +53787,7 @@
53787
53787
 
53788
53788
  const FILEBASENAME = Symbol('fileBasename');
53789
53789
  const FILENAME = Symbol('filename');
53790
- function getParser(inputPathPattern, inputFilePath, naturalData, dicomPS315EOptions, columnMappings, additionalData) {
53790
+ function getParser(inputPathPattern, inputFilePath, naturalData, dicomPS315EOptions, columnMappings, additionalData, naturalMetaData) {
53791
53791
  function protectUid$1(uid) {
53792
53792
  let protectedUid = uid;
53793
53793
  if (dicomPS315EOptions !== 'Off') {
@@ -53847,6 +53847,24 @@
53847
53847
  return getCsvMapping(columnMappings, mapping, key, value);
53848
53848
  };
53849
53849
  })();
53850
+ // Reads the file meta information group (group 0002), which getDicom cannot
53851
+ // see because it operates on the dataset alone. Needed to identify a file by
53852
+ // MediaStorageSOPClassUID (e.g. for a DICOMDIR disguised as a .DCM).
53853
+ //
53854
+ // Values are always the ORIGINAL ones, even in postExclude where getDicom has
53855
+ // already switched to de-identified values — the meta group is captured once
53856
+ // and never re-read. Only string values are returned; a non-string meta value
53857
+ // (e.g. the OB FileMetaInformationVersion) reads as undefined.
53858
+ function getMetaDicom(attrName) {
53859
+ if (!naturalMetaData)
53860
+ return undefined;
53861
+ if (attrName in data$1.DicomMetaDictionary.dictionary) {
53862
+ // if in hex like "(0002,0002)", convert to text key
53863
+ attrName = data$1.DicomMetaDictionary.dictionary[attrName].name;
53864
+ }
53865
+ const value = lodashExports.get(naturalMetaData, attrName);
53866
+ return typeof value === 'string' ? value : undefined;
53867
+ }
53850
53868
  function missingDicom(attrName) {
53851
53869
  const value = getDicom(attrName);
53852
53870
  return typeof value === 'undefined' || value === '';
@@ -53856,6 +53874,7 @@
53856
53874
  getFilePathComp,
53857
53875
  getMapping,
53858
53876
  getDicom,
53877
+ getMetaDicom,
53859
53878
  missingDicom,
53860
53879
  protectUid: protectUid$1,
53861
53880
  // TODO: Phase this out in favor of ISO8601 duration handling.
@@ -53902,15 +53921,31 @@
53902
53921
  }
53903
53922
  // Make make the naturalized data so parser code operates on with tags not hex
53904
53923
  const naturalData = data$1.DicomMetaDictionary.naturalizeDataset(dicomData.dict);
53924
+ // File meta group (0002), kept separate from the dataset so preExclude can
53925
+ // reach MediaStorageSOPClassUID. Absent on a file that was not read from
53926
+ // Part 10 bytes, so guard rather than assume.
53927
+ const naturalMetaData = dicomData.meta
53928
+ ? data$1.DicomMetaDictionary.naturalizeDataset(dicomData.meta)
53929
+ : undefined;
53905
53930
  mapResults.sourceInstanceUID = naturalData.SOPInstanceUID;
53906
53931
  const finalSpec = composeSpecs(mappingOptions.curationSpec());
53907
53932
  // create a parser object to be used in the eval'ed mappingFunctions
53908
- const parser = getParser(finalSpec.inputPathPattern, inputFilePath, naturalData, finalSpec.dicomPS315EOptions, mappingOptions.columnMappings, finalSpec.additionalData);
53909
- // preExclude: original DICOM tags visible via parser.getDicom
53933
+ const parser = getParser(finalSpec.inputPathPattern, inputFilePath, naturalData, finalSpec.dicomPS315EOptions, mappingOptions.columnMappings, finalSpec.additionalData, naturalMetaData);
53934
+ // preExclude: original DICOM tags visible via parser.getDicom / parser.getMetaDicom
53910
53935
  let preExcludeError;
53911
53936
  try {
53912
53937
  if (finalSpec.preExclude?.(parser)) {
53913
53938
  mapResults.excluded = 'pre';
53939
+ // Write pass only: collectMappings runs on both the form-generation pass
53940
+ // (skipWrite) and the write pass, so emitting on both would double-count
53941
+ // the file for anomaly consumers. Matches the benign scan-exclusion
53942
+ // anomalies, which are likewise write-pass only.
53943
+ //
53944
+ // Name only, never the full path: anomalies are shared with the
53945
+ // server-bound log, and the raw path is carried in fileInfo instead.
53946
+ if (!mappingOptions.skipWrite) {
53947
+ mapResults.anomalies.push(`Skipped pre-excluded file: ${parser.getFilePathComp(parser.FILENAME)}`);
53948
+ }
53914
53949
  return [naturalData, mapResults];
53915
53950
  }
53916
53951
  }
@@ -54002,13 +54037,18 @@
54002
54037
  mapResults.outputFilePath = parts.join('/');
54003
54038
  }
54004
54039
  }
54005
- // postExclude: output path is finalised; parser.getDicom() returns de-identified values
54040
+ // postExclude: output path is finalised; parser.getDicom() returns de-identified
54041
+ // values — but parser.getMetaDicom() still returns the original meta group.
54006
54042
  try {
54007
54043
  if (finalSpec.postExclude?.({
54008
54044
  ...parser,
54009
54045
  outputFilePath: mapResults.outputFilePath,
54010
54046
  })) {
54011
54047
  mapResults.excluded = 'post';
54048
+ // Write pass only — see the preExclude note above.
54049
+ if (!mappingOptions.skipWrite) {
54050
+ mapResults.anomalies.push(`Skipped post-excluded file: ${parser.getFilePathComp(parser.FILENAME)}`);
54051
+ }
54012
54052
  return [naturalData, mapResults];
54013
54053
  }
54014
54054
  }
@@ -68839,7 +68879,7 @@
68839
68879
  /* eslint-enable */
68840
68880
 
68841
68881
  var WorkerFactory = /*#__PURE__*/createInlineWorkerFactory(/* rollup-plugin-web-worker-loader */function () {
68842
- (function () {
68882
+ ((function (exports) {
68843
68883
  '__worker_loader_strict__';
68844
68884
 
68845
68885
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
@@ -68900,6 +68940,23 @@
68900
68940
  'thumbs.db',
68901
68941
  '.ds_store',
68902
68942
  ];
68943
+ /**
68944
+ * Name-based exclusion decision for an S3 object key.
68945
+ *
68946
+ * Exported for testing: the S3 listing path needs a live bucket, and the worker
68947
+ * runs in its own process where the S3 client cannot be mocked.
68948
+ *
68949
+ * Unlike the filesystem paths, an S3 key carries its whole prefix while the
68950
+ * exclusion list holds bare filenames — and the defaults apply here too.
68951
+ */
68952
+ function isS3KeyExcludedByName(key, extraExcludedFiletypes, includeDefaults) {
68953
+ const objectName = key.slice(key.lastIndexOf('/') + 1);
68954
+ const allExcludedFiletypes = [
68955
+ ...(includeDefaults ? DEFAULT_EXCLUDED_FILETYPES : []),
68956
+ ...extraExcludedFiletypes,
68957
+ ];
68958
+ return allExcludedFiletypes.some((excluded) => objectName.toLowerCase() === excluded.toLowerCase());
68959
+ }
68903
68960
  /**
68904
68961
  * Build a PHI-safe error message for a file that could not be read during
68905
68962
  * scanning. Only the error code/name is included — never `error.message`,
@@ -69018,7 +69075,7 @@
69018
69075
  return false;
69019
69076
  }
69020
69077
  // Check if the file is in the list of excluded files
69021
- if (excludedFiletypes.some((excluded) => s3Item.Key.toLowerCase() === excluded.toLowerCase())) {
69078
+ if (isS3KeyExcludedByName(s3Item.Key, excludedFiletypes, !noDefaultExclusions)) {
69022
69079
  fileAnomalies.push(`Skipped excluded file: ${s3Item.Key}`);
69023
69080
  return false;
69024
69081
  }
@@ -69564,7 +69621,11 @@
69564
69621
  __proto__: null
69565
69622
  });
69566
69623
 
69567
- })();
69624
+ exports.isS3KeyExcludedByName = isS3KeyExcludedByName;
69625
+
69626
+ return exports;
69627
+
69628
+ }))({});
69568
69629
 
69569
69630
  });
69570
69631
  /* eslint-enable */
@@ -125511,7 +125572,7 @@
125511
125572
 
125512
125573
  const FILEBASENAME = Symbol('fileBasename');
125513
125574
  const FILENAME = Symbol('filename');
125514
- function getParser(inputPathPattern, inputFilePath, naturalData, dicomPS315EOptions, columnMappings, additionalData) {
125575
+ function getParser(inputPathPattern, inputFilePath, naturalData, dicomPS315EOptions, columnMappings, additionalData, naturalMetaData) {
125515
125576
  function protectUid$1(uid) {
125516
125577
  let protectedUid = uid;
125517
125578
  if (dicomPS315EOptions !== 'Off') {
@@ -125571,6 +125632,24 @@
125571
125632
  return getCsvMapping(columnMappings, mapping, key, value);
125572
125633
  };
125573
125634
  })();
125635
+ // Reads the file meta information group (group 0002), which getDicom cannot
125636
+ // see because it operates on the dataset alone. Needed to identify a file by
125637
+ // MediaStorageSOPClassUID (e.g. for a DICOMDIR disguised as a .DCM).
125638
+ //
125639
+ // Values are always the ORIGINAL ones, even in postExclude where getDicom has
125640
+ // already switched to de-identified values — the meta group is captured once
125641
+ // and never re-read. Only string values are returned; a non-string meta value
125642
+ // (e.g. the OB FileMetaInformationVersion) reads as undefined.
125643
+ function getMetaDicom(attrName) {
125644
+ if (!naturalMetaData)
125645
+ return undefined;
125646
+ if (attrName in data$1.DicomMetaDictionary.dictionary) {
125647
+ // if in hex like "(0002,0002)", convert to text key
125648
+ attrName = data$1.DicomMetaDictionary.dictionary[attrName].name;
125649
+ }
125650
+ const value = lodashExports.get(naturalMetaData, attrName);
125651
+ return typeof value === 'string' ? value : undefined;
125652
+ }
125574
125653
  function missingDicom(attrName) {
125575
125654
  const value = getDicom(attrName);
125576
125655
  return typeof value === 'undefined' || value === '';
@@ -125580,6 +125659,7 @@
125580
125659
  getFilePathComp,
125581
125660
  getMapping,
125582
125661
  getDicom,
125662
+ getMetaDicom,
125583
125663
  missingDicom,
125584
125664
  protectUid: protectUid$1,
125585
125665
  // TODO: Phase this out in favor of ISO8601 duration handling.
@@ -125626,15 +125706,31 @@
125626
125706
  }
125627
125707
  // Make make the naturalized data so parser code operates on with tags not hex
125628
125708
  const naturalData = data$1.DicomMetaDictionary.naturalizeDataset(dicomData.dict);
125709
+ // File meta group (0002), kept separate from the dataset so preExclude can
125710
+ // reach MediaStorageSOPClassUID. Absent on a file that was not read from
125711
+ // Part 10 bytes, so guard rather than assume.
125712
+ const naturalMetaData = dicomData.meta
125713
+ ? data$1.DicomMetaDictionary.naturalizeDataset(dicomData.meta)
125714
+ : undefined;
125629
125715
  mapResults.sourceInstanceUID = naturalData.SOPInstanceUID;
125630
125716
  const finalSpec = composeSpecs(mappingOptions.curationSpec());
125631
125717
  // create a parser object to be used in the eval'ed mappingFunctions
125632
- const parser = getParser(finalSpec.inputPathPattern, inputFilePath, naturalData, finalSpec.dicomPS315EOptions, mappingOptions.columnMappings, finalSpec.additionalData);
125633
- // preExclude: original DICOM tags visible via parser.getDicom
125718
+ const parser = getParser(finalSpec.inputPathPattern, inputFilePath, naturalData, finalSpec.dicomPS315EOptions, mappingOptions.columnMappings, finalSpec.additionalData, naturalMetaData);
125719
+ // preExclude: original DICOM tags visible via parser.getDicom / parser.getMetaDicom
125634
125720
  let preExcludeError;
125635
125721
  try {
125636
125722
  if (finalSpec.preExclude?.(parser)) {
125637
125723
  mapResults.excluded = 'pre';
125724
+ // Write pass only: collectMappings runs on both the form-generation pass
125725
+ // (skipWrite) and the write pass, so emitting on both would double-count
125726
+ // the file for anomaly consumers. Matches the benign scan-exclusion
125727
+ // anomalies, which are likewise write-pass only.
125728
+ //
125729
+ // Name only, never the full path: anomalies are shared with the
125730
+ // server-bound log, and the raw path is carried in fileInfo instead.
125731
+ if (!mappingOptions.skipWrite) {
125732
+ mapResults.anomalies.push(`Skipped pre-excluded file: ${parser.getFilePathComp(parser.FILENAME)}`);
125733
+ }
125638
125734
  return [naturalData, mapResults];
125639
125735
  }
125640
125736
  }
@@ -125726,13 +125822,18 @@
125726
125822
  mapResults.outputFilePath = parts.join('/');
125727
125823
  }
125728
125824
  }
125729
- // postExclude: output path is finalised; parser.getDicom() returns de-identified values
125825
+ // postExclude: output path is finalised; parser.getDicom() returns de-identified
125826
+ // values — but parser.getMetaDicom() still returns the original meta group.
125730
125827
  try {
125731
125828
  if (finalSpec.postExclude?.({
125732
125829
  ...parser,
125733
125830
  outputFilePath: mapResults.outputFilePath,
125734
125831
  })) {
125735
125832
  mapResults.excluded = 'post';
125833
+ // Write pass only — see the preExclude note above.
125834
+ if (!mappingOptions.skipWrite) {
125835
+ mapResults.anomalies.push(`Skipped post-excluded file: ${parser.getFilePathComp(parser.FILENAME)}`);
125836
+ }
125736
125837
  return [naturalData, mapResults];
125737
125838
  }
125738
125839
  }