dicom-synth 1.13.0 → 1.14.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.
@@ -607,9 +607,13 @@ var UID_SWEEP_EXCLUDE = /* @__PURE__ */ new Set([
607
607
  "SeriesInstanceUID",
608
608
  "SOPClassUID"
609
609
  ]);
610
+ var vrCache = /* @__PURE__ */ new Map();
610
611
  function vrOf(keyword) {
612
+ if (vrCache.has(keyword)) return vrCache.get(keyword);
611
613
  const nm = dcmjsAny.data.DicomMetaDictionary.nameMap;
612
- return (nm?.[keyword] ?? nm?.[`RETIRED_${keyword}`])?.vr;
614
+ const vr = (nm?.[keyword] ?? nm?.[`RETIRED_${keyword}`])?.vr;
615
+ vrCache.set(keyword, vr);
616
+ return vr;
613
617
  }
614
618
  function hashUidVia(map, salt, original) {
615
619
  let synthetic = map.get(original);
@@ -619,22 +623,78 @@ function hashUidVia(map, salt, original) {
619
623
  }
620
624
  return synthetic;
621
625
  }
626
+ function isUidLeaf(value) {
627
+ return typeof value === "string" || Array.isArray(value) && value.every((v) => typeof v === "string");
628
+ }
629
+ function hasHashableUid(value) {
630
+ const uids = typeof value === "string" ? [value] : value;
631
+ return uids.some((v) => !v.startsWith(DICOM_ORG_ROOT));
632
+ }
633
+ function pruneUidSequence(value) {
634
+ const items = Array.isArray(value) ? value : [value];
635
+ const pruned = [];
636
+ for (const item of items) {
637
+ if (typeof item !== "object" || item === null || Array.isArray(item))
638
+ continue;
639
+ const skeleton = {};
640
+ let hashable = false;
641
+ for (const [keyword, v] of Object.entries(item)) {
642
+ if (keyword === "_vrMap") continue;
643
+ const vr = vrOf(keyword);
644
+ if (vr === "UI") {
645
+ if (isUidLeaf(v)) {
646
+ skeleton[keyword] = v;
647
+ if (hasHashableUid(v)) hashable = true;
648
+ }
649
+ } else if (vr === "SQ") {
650
+ const nested = pruneUidSequence(v);
651
+ if (nested) {
652
+ skeleton[keyword] = nested;
653
+ hashable = true;
654
+ }
655
+ }
656
+ }
657
+ if (hashable) pruned.push(skeleton);
658
+ }
659
+ return pruned.length > 0 ? pruned : void 0;
660
+ }
622
661
  function collectUidOriginals(record) {
623
662
  const uids = {};
624
663
  let found = false;
625
664
  for (const [keyword, value] of Object.entries(record)) {
626
665
  if (UID_SWEEP_EXCLUDE.has(keyword)) continue;
627
- if (typeof value !== "string" || value.startsWith(DICOM_ORG_ROOT)) continue;
628
- if (vrOf(keyword) !== "UI") continue;
629
- uids[keyword] = value;
630
- found = true;
666
+ const vr = vrOf(keyword);
667
+ if (vr === "UI") {
668
+ if (isUidLeaf(value) && hasHashableUid(value)) {
669
+ uids[keyword] = value;
670
+ found = true;
671
+ }
672
+ } else if (vr === "SQ") {
673
+ const pruned = pruneUidSequence(value);
674
+ if (pruned) {
675
+ uids[keyword] = pruned;
676
+ found = true;
677
+ }
678
+ }
631
679
  }
632
680
  return found ? uids : void 0;
633
681
  }
682
+ function hashUidValue(value, salt, map) {
683
+ if (typeof value === "string") {
684
+ return value.startsWith(DICOM_ORG_ROOT) ? value : hashUidVia(map, salt, value);
685
+ }
686
+ if (Array.isArray(value)) return value.map((v) => hashUidValue(v, salt, map));
687
+ return Object.fromEntries(
688
+ Object.entries(value).map(([k, v]) => [
689
+ k,
690
+ hashUidValue(v, salt, map)
691
+ ])
692
+ );
693
+ }
634
694
  function hashUidTags(uids, salt, map) {
635
695
  const tags = {};
636
696
  for (const [keyword, original] of Object.entries(uids)) {
637
- tags[keyword] = hashUidVia(map, salt, original);
697
+ tags[keyword] = hashUidValue(original, salt, map);
638
698
  }
639
699
  return tags;
640
700
  }
@@ -820,11 +880,17 @@ function describeDirectory(dir, options = {}) {
820
880
  }
821
881
  const salt = preserveUids ? options.uidSalt ?? deriveSalt([...studies.keys()]) : void 0;
822
882
  if (salt !== void 0) {
883
+ const sweptKeepTags = /* @__PURE__ */ new Set();
823
884
  for (const study of studies.values()) {
824
885
  for (const series of study.values()) {
825
886
  if (!Array.isArray(series)) continue;
826
887
  for (const rec of series) {
827
888
  if (rec.uidOriginals) {
889
+ if (rec.tags) {
890
+ for (const keyword of Object.keys(rec.uidOriginals)) {
891
+ if (keyword in rec.tags) sweptKeepTags.add(keyword);
892
+ }
893
+ }
828
894
  rec.tags = mergeTags(
829
895
  rec.tags,
830
896
  hashUidTags(rec.uidOriginals, salt, uidMap)
@@ -833,6 +899,11 @@ function describeDirectory(dir, options = {}) {
833
899
  }
834
900
  }
835
901
  }
902
+ for (const keyword of sweptKeepTags) {
903
+ console.warn(
904
+ `describe: keep-tag "${keyword}" contains UID references \u2014 kept as its hashed UID skeleton, not verbatim (disable with preserveUids: false)`
905
+ );
906
+ }
836
907
  }
837
908
  const withGroupUid = (spec2, seriesUid) => salt === void 0 ? spec2 : {
838
909
  ...spec2,
package/dist/esm/index.js CHANGED
@@ -1562,9 +1562,13 @@ var UID_SWEEP_EXCLUDE = /* @__PURE__ */ new Set([
1562
1562
  "SeriesInstanceUID",
1563
1563
  "SOPClassUID"
1564
1564
  ]);
1565
+ var vrCache = /* @__PURE__ */ new Map();
1565
1566
  function vrOf(keyword) {
1567
+ if (vrCache.has(keyword)) return vrCache.get(keyword);
1566
1568
  const nm = dcmjsAny2.data.DicomMetaDictionary.nameMap;
1567
- return (nm?.[keyword] ?? nm?.[`RETIRED_${keyword}`])?.vr;
1569
+ const vr = (nm?.[keyword] ?? nm?.[`RETIRED_${keyword}`])?.vr;
1570
+ vrCache.set(keyword, vr);
1571
+ return vr;
1568
1572
  }
1569
1573
  function hashUidVia(map, salt, original) {
1570
1574
  let synthetic = map.get(original);
@@ -1574,22 +1578,78 @@ function hashUidVia(map, salt, original) {
1574
1578
  }
1575
1579
  return synthetic;
1576
1580
  }
1581
+ function isUidLeaf(value) {
1582
+ return typeof value === "string" || Array.isArray(value) && value.every((v) => typeof v === "string");
1583
+ }
1584
+ function hasHashableUid(value) {
1585
+ const uids = typeof value === "string" ? [value] : value;
1586
+ return uids.some((v) => !v.startsWith(DICOM_ORG_ROOT));
1587
+ }
1588
+ function pruneUidSequence(value) {
1589
+ const items = Array.isArray(value) ? value : [value];
1590
+ const pruned = [];
1591
+ for (const item of items) {
1592
+ if (typeof item !== "object" || item === null || Array.isArray(item))
1593
+ continue;
1594
+ const skeleton = {};
1595
+ let hashable = false;
1596
+ for (const [keyword, v] of Object.entries(item)) {
1597
+ if (keyword === "_vrMap") continue;
1598
+ const vr = vrOf(keyword);
1599
+ if (vr === "UI") {
1600
+ if (isUidLeaf(v)) {
1601
+ skeleton[keyword] = v;
1602
+ if (hasHashableUid(v)) hashable = true;
1603
+ }
1604
+ } else if (vr === "SQ") {
1605
+ const nested = pruneUidSequence(v);
1606
+ if (nested) {
1607
+ skeleton[keyword] = nested;
1608
+ hashable = true;
1609
+ }
1610
+ }
1611
+ }
1612
+ if (hashable) pruned.push(skeleton);
1613
+ }
1614
+ return pruned.length > 0 ? pruned : void 0;
1615
+ }
1577
1616
  function collectUidOriginals(record) {
1578
1617
  const uids = {};
1579
1618
  let found = false;
1580
1619
  for (const [keyword, value] of Object.entries(record)) {
1581
1620
  if (UID_SWEEP_EXCLUDE.has(keyword)) continue;
1582
- if (typeof value !== "string" || value.startsWith(DICOM_ORG_ROOT)) continue;
1583
- if (vrOf(keyword) !== "UI") continue;
1584
- uids[keyword] = value;
1585
- found = true;
1621
+ const vr = vrOf(keyword);
1622
+ if (vr === "UI") {
1623
+ if (isUidLeaf(value) && hasHashableUid(value)) {
1624
+ uids[keyword] = value;
1625
+ found = true;
1626
+ }
1627
+ } else if (vr === "SQ") {
1628
+ const pruned = pruneUidSequence(value);
1629
+ if (pruned) {
1630
+ uids[keyword] = pruned;
1631
+ found = true;
1632
+ }
1633
+ }
1586
1634
  }
1587
1635
  return found ? uids : void 0;
1588
1636
  }
1637
+ function hashUidValue(value, salt, map) {
1638
+ if (typeof value === "string") {
1639
+ return value.startsWith(DICOM_ORG_ROOT) ? value : hashUidVia(map, salt, value);
1640
+ }
1641
+ if (Array.isArray(value)) return value.map((v) => hashUidValue(v, salt, map));
1642
+ return Object.fromEntries(
1643
+ Object.entries(value).map(([k, v]) => [
1644
+ k,
1645
+ hashUidValue(v, salt, map)
1646
+ ])
1647
+ );
1648
+ }
1589
1649
  function hashUidTags(uids, salt, map) {
1590
1650
  const tags = {};
1591
1651
  for (const [keyword, original] of Object.entries(uids)) {
1592
- tags[keyword] = hashUidVia(map, salt, original);
1652
+ tags[keyword] = hashUidValue(original, salt, map);
1593
1653
  }
1594
1654
  return tags;
1595
1655
  }
@@ -1775,11 +1835,17 @@ function describeDirectory(dir, options = {}) {
1775
1835
  }
1776
1836
  const salt = preserveUids ? options.uidSalt ?? deriveSalt([...studies.keys()]) : void 0;
1777
1837
  if (salt !== void 0) {
1838
+ const sweptKeepTags = /* @__PURE__ */ new Set();
1778
1839
  for (const study of studies.values()) {
1779
1840
  for (const series of study.values()) {
1780
1841
  if (!Array.isArray(series)) continue;
1781
1842
  for (const rec of series) {
1782
1843
  if (rec.uidOriginals) {
1844
+ if (rec.tags) {
1845
+ for (const keyword of Object.keys(rec.uidOriginals)) {
1846
+ if (keyword in rec.tags) sweptKeepTags.add(keyword);
1847
+ }
1848
+ }
1783
1849
  rec.tags = mergeTags(
1784
1850
  rec.tags,
1785
1851
  hashUidTags(rec.uidOriginals, salt, uidMap)
@@ -1788,6 +1854,11 @@ function describeDirectory(dir, options = {}) {
1788
1854
  }
1789
1855
  }
1790
1856
  }
1857
+ for (const keyword of sweptKeepTags) {
1858
+ console.warn(
1859
+ `describe: keep-tag "${keyword}" contains UID references \u2014 kept as its hashed UID skeleton, not verbatim (disable with preserveUids: false)`
1860
+ );
1861
+ }
1791
1862
  }
1792
1863
  const withGroupUid = (spec2, seriesUid) => salt === void 0 ? spec2 : {
1793
1864
  ...spec2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dicom-synth",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "Toolkit for synthetic DICOM fixtures and public fixture fetch/cache.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",