@ubercode/dcmtk 0.1.1 → 0.2.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.
Files changed (41) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +18 -15
  3. package/dist/DicomInstance-By9zd7GM.d.cts +625 -0
  4. package/dist/DicomInstance-CQEIuF_x.d.ts +625 -0
  5. package/dist/{dcmodify-CTXBWKU9.d.cts → dcmodify-B-_uUIKB.d.ts} +4 -2
  6. package/dist/{dcmodify-Daeafqrm.d.ts → dcmodify-Gds9u5Vj.d.cts} +4 -2
  7. package/dist/dicom.cjs +329 -51
  8. package/dist/dicom.cjs.map +1 -1
  9. package/dist/dicom.d.cts +368 -3
  10. package/dist/dicom.d.ts +368 -3
  11. package/dist/dicom.js +329 -51
  12. package/dist/dicom.js.map +1 -1
  13. package/dist/index.cjs +1460 -419
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +8 -7
  16. package/dist/index.d.ts +8 -7
  17. package/dist/index.js +1432 -413
  18. package/dist/index.js.map +1 -1
  19. package/dist/servers.cjs +2379 -196
  20. package/dist/servers.cjs.map +1 -1
  21. package/dist/servers.d.cts +1654 -3
  22. package/dist/servers.d.ts +1654 -3
  23. package/dist/servers.js +2305 -145
  24. package/dist/servers.js.map +1 -1
  25. package/dist/tools.cjs +97 -50
  26. package/dist/tools.cjs.map +1 -1
  27. package/dist/tools.d.cts +21 -4
  28. package/dist/tools.d.ts +21 -4
  29. package/dist/tools.js +97 -51
  30. package/dist/tools.js.map +1 -1
  31. package/dist/{types-zHhxS7d2.d.cts → types-Cgumy1N4.d.cts} +1 -24
  32. package/dist/{types-zHhxS7d2.d.ts → types-Cgumy1N4.d.ts} +1 -24
  33. package/dist/utils.cjs.map +1 -1
  34. package/dist/utils.d.cts +1 -1
  35. package/dist/utils.d.ts +1 -1
  36. package/dist/utils.js.map +1 -1
  37. package/package.json +8 -8
  38. package/dist/index-BZxi4104.d.ts +0 -826
  39. package/dist/index-CapkWqxy.d.ts +0 -1295
  40. package/dist/index-DX4C3zbo.d.cts +0 -826
  41. package/dist/index-r7AvpkCE.d.cts +0 -1295
package/dist/index.cjs CHANGED
@@ -2,18 +2,41 @@
2
2
 
3
3
  var path = require('path');
4
4
  var zod = require('zod');
5
- var fs = require('fs');
5
+ var fs$1 = require('fs');
6
6
  var child_process = require('child_process');
7
7
  var kill = require('tree-kill');
8
8
  var events = require('events');
9
9
  var stderrLib = require('stderr-lib');
10
- var promises = require('fs/promises');
11
10
  var fastXmlParser = require('fast-xml-parser');
11
+ var fs = require('fs/promises');
12
+ var net = require('net');
12
13
  var os = require('os');
13
14
 
14
15
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
15
16
 
17
+ function _interopNamespace(e) {
18
+ if (e && e.__esModule) return e;
19
+ var n = Object.create(null);
20
+ if (e) {
21
+ Object.keys(e).forEach(function (k) {
22
+ if (k !== 'default') {
23
+ var d = Object.getOwnPropertyDescriptor(e, k);
24
+ Object.defineProperty(n, k, d.get ? d : {
25
+ enumerable: true,
26
+ get: function () { return e[k]; }
27
+ });
28
+ }
29
+ });
30
+ }
31
+ n.default = e;
32
+ return Object.freeze(n);
33
+ }
34
+
35
+ var path__namespace = /*#__PURE__*/_interopNamespace(path);
16
36
  var kill__default = /*#__PURE__*/_interopDefault(kill);
37
+ var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
38
+ var net__namespace = /*#__PURE__*/_interopNamespace(net);
39
+ var os__namespace = /*#__PURE__*/_interopNamespace(os);
17
40
 
18
41
  var __defProp = Object.defineProperty;
19
42
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -29,12 +52,6 @@ function err(error) {
29
52
  function assertUnreachable(x) {
30
53
  throw new Error(`Exhaustive check failed: ${JSON.stringify(x)}`);
31
54
  }
32
- function unwrap(result) {
33
- if (result.ok) {
34
- return result.value;
35
- }
36
- throw result.error;
37
- }
38
55
  function mapResult(result, fn) {
39
56
  if (result.ok) {
40
57
  return ok(fn(result.value));
@@ -124,6 +141,11 @@ function createPort(input) {
124
141
  }
125
142
  return ok(input);
126
143
  }
144
+ function tag(input) {
145
+ const result = createDicomTagPath(input);
146
+ if (!result.ok) throw result.error;
147
+ return result.value;
148
+ }
127
149
  var AETitleSchema = zod.z.string().min(AE_TITLE_MIN_LENGTH).max(AE_TITLE_MAX_LENGTH).regex(AE_TITLE_PATTERN);
128
150
  var PortSchema = zod.z.number().int().min(PORT_MIN).max(PORT_MAX);
129
151
  var DicomTagSchema = zod.z.string().regex(DICOM_TAG_PATTERN);
@@ -201,7 +223,7 @@ function binaryName(name) {
201
223
  }
202
224
  function hasRequiredBinaries(dir) {
203
225
  for (const bin of REQUIRED_BINARIES) {
204
- if (!fs.existsSync(path.join(dir, binaryName(bin)))) {
226
+ if (!fs$1.existsSync(path.join(dir, binaryName(bin)))) {
205
227
  return false;
206
228
  }
207
229
  }
@@ -30259,8 +30281,8 @@ var dictionary_default = {
30259
30281
 
30260
30282
  // src/dicom/dictionary.ts
30261
30283
  var dictionary = dictionary_default;
30262
- function lookupTag(tag) {
30263
- const key = tag.includes(",") ? tag.replace(/[(),]/g, "") : tag;
30284
+ function lookupTag(tag2) {
30285
+ const key = tag2.includes(",") ? tag2.replace(/[(),]/g, "") : tag2;
30264
30286
  return dictionary[key.toUpperCase()];
30265
30287
  }
30266
30288
  var nameIndex;
@@ -30427,13 +30449,13 @@ function advancePastMatch(remaining, matchLength) {
30427
30449
  if (after.length === 1) throw new Error("Tag path cannot end with a dot separator");
30428
30450
  return after.slice(1);
30429
30451
  }
30430
- function tagPathToSegments(path) {
30452
+ function tagPathToSegments(path2) {
30431
30453
  const segments = [];
30432
- let remaining = path;
30454
+ let remaining = path2;
30433
30455
  for (let i = 0; i < MAX_TRAVERSAL_DEPTH && remaining.length > 0; i++) {
30434
30456
  const match = SEGMENT_PATTERN.exec(remaining);
30435
30457
  if (match === null) {
30436
- throw new Error(`Invalid tag path segment at position ${path.length - remaining.length}: "${remaining}"`);
30458
+ throw new Error(`Invalid tag path segment at position ${path2.length - remaining.length}: "${remaining}"`);
30437
30459
  }
30438
30460
  segments.push(matchToSegment(match));
30439
30461
  remaining = advancePastMatch(remaining, match[0].length);
@@ -30474,18 +30496,18 @@ function isValidDicomJsonModel(obj) {
30474
30496
  }
30475
30497
  var TAG_WITH_PARENS = /^\(([0-9A-Fa-f]{4}),([0-9A-Fa-f]{4})\)$/;
30476
30498
  var TAG_HEX_ONLY = /^[0-9A-Fa-f]{8}$/;
30477
- function normalizeTag(tag) {
30478
- const parenMatch = TAG_WITH_PARENS.exec(tag);
30499
+ function normalizeTag(tag2) {
30500
+ const parenMatch = TAG_WITH_PARENS.exec(tag2);
30479
30501
  if (parenMatch !== null) {
30480
30502
  const group = parenMatch[1];
30481
30503
  const element = parenMatch[2];
30482
- if (group === void 0 || element === void 0) return err(new Error(`Invalid tag: "${tag}"`));
30504
+ if (group === void 0 || element === void 0) return err(new Error(`Invalid tag: "${tag2}"`));
30483
30505
  return ok(`${group}${element}`.toUpperCase());
30484
30506
  }
30485
- if (TAG_HEX_ONLY.test(tag)) {
30486
- return ok(tag.toUpperCase());
30507
+ if (TAG_HEX_ONLY.test(tag2)) {
30508
+ return ok(tag2.toUpperCase());
30487
30509
  }
30488
- return err(new Error(`Invalid tag format: "${tag}". Expected (XXXX,XXXX) or XXXXXXXX`));
30510
+ return err(new Error(`Invalid tag format: "${tag2}". Expected (XXXX,XXXX) or XXXXXXXX`));
30489
30511
  }
30490
30512
  function resolveElement(data, tagKey) {
30491
30513
  return data[tagKey];
@@ -30666,11 +30688,11 @@ var DicomDataset = class _DicomDataset {
30666
30688
  * @param tag - A DicomTag `(0010,0010)` or hex string `00100010`
30667
30689
  * @returns Result containing the element or an error if not found
30668
30690
  */
30669
- getElement(tag) {
30670
- const norm = normalizeTag(tag);
30691
+ getElement(tag2) {
30692
+ const norm = normalizeTag(tag2);
30671
30693
  if (!norm.ok) return err(norm.error);
30672
30694
  const element = resolveElement(this.data, norm.value);
30673
- if (element === void 0) return err(new Error(`Tag ${tag} not found`));
30695
+ if (element === void 0) return err(new Error(`Tag ${tag2} not found`));
30674
30696
  return ok(element);
30675
30697
  }
30676
30698
  /**
@@ -30679,11 +30701,11 @@ var DicomDataset = class _DicomDataset {
30679
30701
  * @param tag - A DicomTag `(0010,0010)` or hex string `00100010`
30680
30702
  * @returns Result containing the readonly Value array or an error
30681
30703
  */
30682
- getValue(tag) {
30683
- const elemResult = this.getElement(tag);
30704
+ getValue(tag2) {
30705
+ const elemResult = this.getElement(tag2);
30684
30706
  if (!elemResult.ok) return err(elemResult.error);
30685
30707
  const values = elemResult.value.Value;
30686
- if (values === void 0) return err(new Error(`Tag ${tag} has no Value`));
30708
+ if (values === void 0) return err(new Error(`Tag ${tag2} has no Value`));
30687
30709
  return ok(values);
30688
30710
  }
30689
30711
  /**
@@ -30692,11 +30714,11 @@ var DicomDataset = class _DicomDataset {
30692
30714
  * @param tag - A DicomTag `(0010,0010)` or hex string `00100010`
30693
30715
  * @returns Result containing the first value or an error
30694
30716
  */
30695
- getFirstValue(tag) {
30696
- const valResult = this.getValue(tag);
30717
+ getFirstValue(tag2) {
30718
+ const valResult = this.getValue(tag2);
30697
30719
  if (!valResult.ok) return err(valResult.error);
30698
30720
  const first = valResult.value[0];
30699
- if (first === void 0) return err(new Error(`Tag ${tag} Value array is empty`));
30721
+ if (first === void 0) return err(new Error(`Tag ${tag2} Value array is empty`));
30700
30722
  return ok(first);
30701
30723
  }
30702
30724
  /**
@@ -30709,8 +30731,8 @@ var DicomDataset = class _DicomDataset {
30709
30731
  * @param fallback - Value to return if tag is missing (default: `''`)
30710
30732
  * @returns The string value or the fallback
30711
30733
  */
30712
- getString(tag, fallback = "") {
30713
- const elemResult = this.getElement(tag);
30734
+ getString(tag2, fallback = "") {
30735
+ const elemResult = this.getElement(tag2);
30714
30736
  if (!elemResult.ok) return fallback;
30715
30737
  const str = extractString(elemResult.value);
30716
30738
  return str.length > 0 ? str : fallback;
@@ -30721,8 +30743,8 @@ var DicomDataset = class _DicomDataset {
30721
30743
  * @param tag - A DicomTag `(0010,0010)` or hex string `00100010`
30722
30744
  * @returns Result containing the number or an error
30723
30745
  */
30724
- getNumber(tag) {
30725
- const elemResult = this.getElement(tag);
30746
+ getNumber(tag2) {
30747
+ const elemResult = this.getElement(tag2);
30726
30748
  if (!elemResult.ok) return err(elemResult.error);
30727
30749
  return extractNumber(elemResult.value);
30728
30750
  }
@@ -30734,8 +30756,8 @@ var DicomDataset = class _DicomDataset {
30734
30756
  * @param tag - A DicomTag `(0010,0010)` or hex string `00100010`
30735
30757
  * @returns Result containing the readonly string array or an error
30736
30758
  */
30737
- getStrings(tag) {
30738
- const elemResult = this.getElement(tag);
30759
+ getStrings(tag2) {
30760
+ const elemResult = this.getElement(tag2);
30739
30761
  if (!elemResult.ok) return err(elemResult.error);
30740
30762
  return ok(extractStrings(elemResult.value));
30741
30763
  }
@@ -30745,8 +30767,8 @@ var DicomDataset = class _DicomDataset {
30745
30767
  * @param tag - A DicomTag `(0010,0010)` or hex string `00100010`
30746
30768
  * @returns `true` if the tag is present
30747
30769
  */
30748
- hasTag(tag) {
30749
- const norm = normalizeTag(tag);
30770
+ hasTag(tag2) {
30771
+ const norm = normalizeTag(tag2);
30750
30772
  if (!norm.ok) return false;
30751
30773
  return resolveElement(this.data, norm.value) !== void 0;
30752
30774
  }
@@ -30756,10 +30778,10 @@ var DicomDataset = class _DicomDataset {
30756
30778
  * @param path - A branded DicomTagPath, e.g. `(0040,A730)[0].(0040,A160)`
30757
30779
  * @returns Result containing the element at the path or an error
30758
30780
  */
30759
- getElementAtPath(path) {
30781
+ getElementAtPath(path2) {
30760
30782
  let segments;
30761
30783
  try {
30762
- segments = tagPathToSegments(path);
30784
+ segments = tagPathToSegments(path2);
30763
30785
  } catch (e) {
30764
30786
  return err(stderrLib.stderr(e));
30765
30787
  }
@@ -30776,10 +30798,10 @@ var DicomDataset = class _DicomDataset {
30776
30798
  * @param path - A branded DicomTagPath, e.g. `(0040,A730)[*].(0040,A160)`
30777
30799
  * @returns A readonly array of all matching values (may be empty)
30778
30800
  */
30779
- findValues(path) {
30801
+ findValues(path2) {
30780
30802
  let segments;
30781
30803
  try {
30782
- segments = tagPathToSegments(path);
30804
+ segments = tagPathToSegments(path2);
30783
30805
  } catch {
30784
30806
  return [];
30785
30807
  }
@@ -30862,6 +30884,18 @@ function buildMergedModifications(base, other, erasures) {
30862
30884
  }
30863
30885
  return merged;
30864
30886
  }
30887
+ function applyBatchEntries(initial, entries) {
30888
+ const keys = Object.keys(entries);
30889
+ let cs = initial;
30890
+ for (let i = 0; i < keys.length; i++) {
30891
+ const key = keys[i];
30892
+ if (key === void 0) continue;
30893
+ const value = entries[key];
30894
+ if (value === void 0) continue;
30895
+ cs = cs.setTag(key, value);
30896
+ }
30897
+ return cs;
30898
+ }
30865
30899
  var ChangeSet = class _ChangeSet {
30866
30900
  constructor(mods, erasures) {
30867
30901
  __publicField(this, "mods");
@@ -30879,22 +30913,22 @@ var ChangeSet = class _ChangeSet {
30879
30913
  * Control characters (except LF/CR) are stripped from the value.
30880
30914
  * If the tag was previously erased, it is removed from the erasure set.
30881
30915
  *
30882
- * @param path - The DICOM tag path to set
30916
+ * @param path - The DICOM tag path to set (e.g. `'(0010,0010)'`)
30883
30917
  * @param value - The new value for the tag
30884
30918
  * @returns A new ChangeSet with the modification applied
30885
30919
  * @throws Error if operation count would exceed MAX_CHANGESET_OPERATIONS
30886
30920
  */
30887
- setTag(path, value) {
30921
+ setTag(path2, value) {
30888
30922
  const totalOps = this.mods.size + this.erased.size;
30889
30923
  if (totalOps >= MAX_CHANGESET_OPERATIONS) {
30890
30924
  throw new Error(`ChangeSet operation limit (${MAX_CHANGESET_OPERATIONS}) exceeded`);
30891
30925
  }
30892
- tagPathToSegments(path);
30926
+ tagPathToSegments(path2);
30893
30927
  const sanitized = sanitizeValue(value);
30894
30928
  const newMods = new Map(this.mods);
30895
- newMods.set(path, sanitized);
30929
+ newMods.set(path2, sanitized);
30896
30930
  const newErasures = new Set(this.erased);
30897
- newErasures.delete(path);
30931
+ newErasures.delete(path2);
30898
30932
  return new _ChangeSet(newMods, newErasures);
30899
30933
  }
30900
30934
  /**
@@ -30902,20 +30936,20 @@ var ChangeSet = class _ChangeSet {
30902
30936
  *
30903
30937
  * If the tag was previously set, the modification is removed.
30904
30938
  *
30905
- * @param path - The DICOM tag path to erase
30939
+ * @param path - The DICOM tag path to erase (e.g. `'(0010,0010)'`)
30906
30940
  * @returns A new ChangeSet with the erasure applied
30907
30941
  * @throws Error if operation count would exceed MAX_CHANGESET_OPERATIONS
30908
30942
  */
30909
- eraseTag(path) {
30943
+ eraseTag(path2) {
30910
30944
  const totalOps = this.mods.size + this.erased.size;
30911
30945
  if (totalOps >= MAX_CHANGESET_OPERATIONS) {
30912
30946
  throw new Error(`ChangeSet operation limit (${MAX_CHANGESET_OPERATIONS}) exceeded`);
30913
30947
  }
30914
- tagPathToSegments(path);
30948
+ tagPathToSegments(path2);
30915
30949
  const newMods = new Map(this.mods);
30916
- newMods.delete(path);
30950
+ newMods.delete(path2);
30917
30951
  const newErasures = new Set(this.erased);
30918
- newErasures.add(path);
30952
+ newErasures.add(path2);
30919
30953
  return new _ChangeSet(newMods, newErasures);
30920
30954
  }
30921
30955
  /**
@@ -30933,6 +30967,50 @@ var ChangeSet = class _ChangeSet {
30933
30967
  newErasures.add(ERASE_PRIVATE_SENTINEL);
30934
30968
  return new _ChangeSet(new Map(this.mods), newErasures);
30935
30969
  }
30970
+ // -----------------------------------------------------------------------
30971
+ // Convenience setters for common DICOM tags
30972
+ // -----------------------------------------------------------------------
30973
+ /** Sets Patient's Name (0010,0010). */
30974
+ setPatientName(value) {
30975
+ return this.setTag("(0010,0010)", value);
30976
+ }
30977
+ /** Sets Patient ID (0010,0020). */
30978
+ setPatientID(value) {
30979
+ return this.setTag("(0010,0020)", value);
30980
+ }
30981
+ /** Sets Study Date (0008,0020). */
30982
+ setStudyDate(value) {
30983
+ return this.setTag("(0008,0020)", value);
30984
+ }
30985
+ /** Sets Modality (0008,0060). */
30986
+ setModality(value) {
30987
+ return this.setTag("(0008,0060)", value);
30988
+ }
30989
+ /** Sets Accession Number (0008,0050). */
30990
+ setAccessionNumber(value) {
30991
+ return this.setTag("(0008,0050)", value);
30992
+ }
30993
+ /** Sets Study Description (0008,1030). */
30994
+ setStudyDescription(value) {
30995
+ return this.setTag("(0008,1030)", value);
30996
+ }
30997
+ /** Sets Series Description (0008,103E). */
30998
+ setSeriesDescription(value) {
30999
+ return this.setTag("(0008,103E)", value);
31000
+ }
31001
+ /** Sets Institution Name (0008,0080). */
31002
+ setInstitutionName(value) {
31003
+ return this.setTag("(0008,0080)", value);
31004
+ }
31005
+ /**
31006
+ * Sets multiple tags at once, returning a new ChangeSet.
31007
+ *
31008
+ * @param entries - A record of tag path → value pairs
31009
+ * @returns A new ChangeSet with all modifications applied
31010
+ */
31011
+ setBatch(entries) {
31012
+ return applyBatchEntries(this, entries);
31013
+ }
30936
31014
  /** All pending tag modifications as a readonly map of path → value. */
30937
31015
  get modifications() {
30938
31016
  return this.mods;
@@ -30975,8 +31053,8 @@ var ChangeSet = class _ChangeSet {
30975
31053
  */
30976
31054
  toModifications() {
30977
31055
  const result = [];
30978
- for (const [tag, value] of this.mods) {
30979
- result.push({ tag, value });
31056
+ for (const [tag2, value] of this.mods) {
31057
+ result.push({ tag: tag2, value });
30980
31058
  }
30981
31059
  return result;
30982
31060
  }
@@ -30990,9 +31068,9 @@ var ChangeSet = class _ChangeSet {
30990
31068
  */
30991
31069
  toErasureArgs() {
30992
31070
  const result = [];
30993
- for (const path of this.erased) {
30994
- if (path !== ERASE_PRIVATE_SENTINEL) {
30995
- result.push(path);
31071
+ for (const path2 of this.erased) {
31072
+ if (path2 !== ERASE_PRIVATE_SENTINEL) {
31073
+ result.push(path2);
30996
31074
  }
30997
31075
  }
30998
31076
  return result;
@@ -31034,12 +31112,72 @@ function createValidationError(toolName, zodError) {
31034
31112
  for (let i = 0; i < zodError.issues.length; i++) {
31035
31113
  const issue = zodError.issues[i];
31036
31114
  if (issue === void 0) continue;
31037
- const path = issue.path.length > 0 ? issue.path.map(String).join(".") : "(root)";
31038
- parts.push(`${path}: ${issue.message}`);
31115
+ const path2 = issue.path.length > 0 ? issue.path.map(String).join(".") : "(root)";
31116
+ parts.push(`${path2}: ${issue.message}`);
31039
31117
  }
31040
31118
  const detail = parts.length > 0 ? parts.join("; ") : "unknown validation error";
31041
31119
  return new Error(`${toolName}: invalid options \u2014 ${detail}`);
31042
31120
  }
31121
+
31122
+ // src/tools/dcm2xml.ts
31123
+ var Dcm2xmlCharset = {
31124
+ /** Use UTF-8 encoding (default). */
31125
+ UTF8: "utf8",
31126
+ /** Use Latin-1 encoding. */
31127
+ LATIN1: "latin1",
31128
+ /** Use ASCII encoding. */
31129
+ ASCII: "ascii"
31130
+ };
31131
+ var Dcm2xmlOptionsSchema = zod.z.object({
31132
+ timeoutMs: zod.z.number().int().positive().optional(),
31133
+ signal: zod.z.instanceof(AbortSignal).optional(),
31134
+ namespace: zod.z.boolean().optional(),
31135
+ charset: zod.z.enum(["utf8", "latin1", "ascii"]).optional(),
31136
+ writeBinaryData: zod.z.boolean().optional(),
31137
+ encodeBinaryBase64: zod.z.boolean().optional()
31138
+ }).strict().optional();
31139
+ function buildArgs(inputPath, options) {
31140
+ const args = [];
31141
+ if (options?.namespace === true) {
31142
+ args.push("+Xn");
31143
+ }
31144
+ if (options?.charset === "latin1") {
31145
+ args.push("+Cl");
31146
+ } else if (options?.charset === "ascii") {
31147
+ args.push("+Ca");
31148
+ }
31149
+ if (options?.writeBinaryData === true) {
31150
+ args.push("+Wb");
31151
+ if (options.encodeBinaryBase64 !== false) {
31152
+ args.push("+Eb");
31153
+ }
31154
+ }
31155
+ args.push(inputPath);
31156
+ return args;
31157
+ }
31158
+ async function dcm2xml(inputPath, options) {
31159
+ const validation = Dcm2xmlOptionsSchema.safeParse(options);
31160
+ if (!validation.success) {
31161
+ return err(createValidationError("dcm2xml", validation.error));
31162
+ }
31163
+ const binaryResult = resolveBinary("dcm2xml");
31164
+ if (!binaryResult.ok) {
31165
+ return err(binaryResult.error);
31166
+ }
31167
+ const args = buildArgs(inputPath, options);
31168
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
31169
+ const result = await execCommand(binaryResult.value, args, {
31170
+ timeoutMs,
31171
+ signal: options?.signal
31172
+ });
31173
+ if (!result.ok) {
31174
+ return err(result.error);
31175
+ }
31176
+ if (result.value.exitCode !== 0) {
31177
+ return err(createToolError("dcm2xml", args, result.value.exitCode, result.value.stderr));
31178
+ }
31179
+ return ok({ xml: result.value.stdout });
31180
+ }
31043
31181
  var PN_REPS = ["Alphabetic", "Ideographic", "Phonetic"];
31044
31182
  var ARRAY_TAG_NAMES = /* @__PURE__ */ new Set(["DicomAttribute", "Value", "PersonName", "Item"]);
31045
31183
  var KNOWN_VR_CODES = /* @__PURE__ */ new Set([
@@ -31175,9 +31313,9 @@ function convertAttributes(obj) {
31175
31313
  for (const attr of attrs) {
31176
31314
  if (typeof attr !== "object" || attr === null) continue;
31177
31315
  const xmlAttr = attr;
31178
- const tag = xmlAttr["@_tag"];
31179
- if (tag === void 0) continue;
31180
- result[tag] = convertElement(xmlAttr);
31316
+ const tag2 = xmlAttr["@_tag"];
31317
+ if (tag2 === void 0) continue;
31318
+ result[tag2] = convertElement(xmlAttr);
31181
31319
  }
31182
31320
  return result;
31183
31321
  }
@@ -31300,263 +31438,6 @@ async function dcm2json(inputPath, options) {
31300
31438
  }
31301
31439
  return tryDirectPath(inputPath, timeoutMs, signal);
31302
31440
  }
31303
- var TAG_OR_PATH_PATTERN = /^\([0-9A-Fa-f]{4},[0-9A-Fa-f]{4}\)(\[\d+\](\.\([0-9A-Fa-f]{4},[0-9A-Fa-f]{4}\)(\[\d+\])?)*)?$/;
31304
- var TagModificationSchema = zod.z.object({
31305
- tag: zod.z.string().regex(TAG_OR_PATH_PATTERN),
31306
- value: zod.z.string()
31307
- });
31308
- var DcmodifyOptionsSchema = zod.z.object({
31309
- timeoutMs: zod.z.number().int().positive().optional(),
31310
- signal: zod.z.instanceof(AbortSignal).optional(),
31311
- modifications: zod.z.array(TagModificationSchema).optional().default([]),
31312
- erasures: zod.z.array(zod.z.string()).optional(),
31313
- erasePrivateTags: zod.z.boolean().optional(),
31314
- noBackup: zod.z.boolean().optional(),
31315
- insertIfMissing: zod.z.boolean().optional()
31316
- }).strict().refine((data) => data.modifications.length > 0 || data.erasures !== void 0 && data.erasures.length > 0 || data.erasePrivateTags === true, {
31317
- message: "At least one of modifications, erasures, or erasePrivateTags is required"
31318
- });
31319
- function buildArgs(inputPath, options) {
31320
- const args = [];
31321
- if (options.noBackup !== false) {
31322
- args.push("-nb");
31323
- }
31324
- const flag = options.insertIfMissing === true ? "-i" : "-m";
31325
- const modifications = options.modifications ?? [];
31326
- for (const mod of modifications) {
31327
- args.push(flag, `${mod.tag}=${mod.value}`);
31328
- }
31329
- if (options.erasures !== void 0) {
31330
- for (const erasure of options.erasures) {
31331
- args.push("-e", erasure);
31332
- }
31333
- }
31334
- if (options.erasePrivateTags === true) {
31335
- args.push("-ep");
31336
- }
31337
- args.push(inputPath);
31338
- return args;
31339
- }
31340
- async function dcmodify(inputPath, options) {
31341
- const validation = DcmodifyOptionsSchema.safeParse(options);
31342
- if (!validation.success) {
31343
- return err(new Error(`dcmodify: invalid options: ${validation.error.message}`));
31344
- }
31345
- const binaryResult = resolveBinary("dcmodify");
31346
- if (!binaryResult.ok) {
31347
- return err(binaryResult.error);
31348
- }
31349
- const args = buildArgs(inputPath, options);
31350
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
31351
- const result = await spawnCommand(binaryResult.value, args, {
31352
- timeoutMs,
31353
- signal: options.signal
31354
- });
31355
- if (!result.ok) {
31356
- return err(result.error);
31357
- }
31358
- if (result.value.exitCode !== 0) {
31359
- return err(createToolError("dcmodify", args, result.value.exitCode, result.value.stderr));
31360
- }
31361
- return ok({ filePath: inputPath });
31362
- }
31363
-
31364
- // src/dicom/DicomFile.ts
31365
- async function applyModifications(filePath, changeset, options) {
31366
- const modifications = changeset.toModifications();
31367
- const erasures = changeset.toErasureArgs();
31368
- const result = await dcmodify(filePath, {
31369
- modifications: modifications.length > 0 ? modifications : void 0,
31370
- erasures: erasures.length > 0 ? erasures : void 0,
31371
- erasePrivateTags: changeset.erasePrivate || void 0,
31372
- insertIfMissing: true,
31373
- timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
31374
- signal: options.signal
31375
- });
31376
- if (!result.ok) return err(result.error);
31377
- return ok(void 0);
31378
- }
31379
- async function copyFileSafe(source, dest) {
31380
- return stderrLib.tryCatch(
31381
- () => promises.copyFile(source, dest),
31382
- (e) => new Error(`Failed to copy file: ${e.message}`)
31383
- );
31384
- }
31385
- async function statFileSize(path) {
31386
- return stderrLib.tryCatch(
31387
- async () => (await promises.stat(path)).size,
31388
- (e) => new Error(`Failed to stat file: ${e.message}`)
31389
- );
31390
- }
31391
- async function unlinkFile(path) {
31392
- return stderrLib.tryCatch(
31393
- () => promises.unlink(path),
31394
- (e) => new Error(`Failed to delete file: ${e.message}`)
31395
- );
31396
- }
31397
- var DicomFile = class _DicomFile {
31398
- constructor(dataset, filePath, changes) {
31399
- /** The immutable DICOM dataset read from the file. */
31400
- __publicField(this, "dataset");
31401
- /** The branded file path. */
31402
- __publicField(this, "filePath");
31403
- /** The accumulated pending changes. */
31404
- __publicField(this, "changes");
31405
- this.dataset = dataset;
31406
- this.filePath = filePath;
31407
- this.changes = changes;
31408
- }
31409
- /**
31410
- * Opens a DICOM file and reads its dataset.
31411
- *
31412
- * @param path - Filesystem path to the DICOM file
31413
- * @param options - Timeout and abort options
31414
- * @returns A Result containing the DicomFile or an error
31415
- */
31416
- static async open(path, options) {
31417
- const filePathResult = createDicomFilePath(path);
31418
- if (!filePathResult.ok) return err(filePathResult.error);
31419
- const jsonResult = await dcm2json(path, {
31420
- timeoutMs: options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
31421
- signal: options?.signal
31422
- });
31423
- if (!jsonResult.ok) return err(jsonResult.error);
31424
- const datasetResult = DicomDataset.fromJson(jsonResult.value.data);
31425
- if (!datasetResult.ok) return err(datasetResult.error);
31426
- return ok(new _DicomFile(datasetResult.value, filePathResult.value, ChangeSet.empty()));
31427
- }
31428
- /**
31429
- * Returns a new DicomFile with the given changes merged into the pending changes.
31430
- *
31431
- * @param changes - A ChangeSet to merge with existing pending changes
31432
- * @returns A new DicomFile with accumulated changes
31433
- */
31434
- withChanges(changes) {
31435
- return new _DicomFile(this.dataset, this.filePath, this.changes.merge(changes));
31436
- }
31437
- /**
31438
- * Returns a new DicomFile with a different file path.
31439
- *
31440
- * Preserves the dataset and pending changes.
31441
- *
31442
- * @param newPath - The new branded file path
31443
- * @returns A new DicomFile pointing to the new path
31444
- */
31445
- withFilePath(newPath) {
31446
- return new _DicomFile(this.dataset, newPath, this.changes);
31447
- }
31448
- /**
31449
- * Applies pending changes to the file in-place using dcmodify.
31450
- *
31451
- * If there are no pending changes, this is a no-op that returns success.
31452
- * After applying, the dataset is NOT refreshed — call {@link DicomFile.open}
31453
- * again if you need fresh data.
31454
- *
31455
- * @param options - Timeout and abort options
31456
- * @returns A Result indicating success or failure
31457
- */
31458
- async applyChanges(options) {
31459
- if (this.changes.isEmpty) return ok(void 0);
31460
- return applyModifications(this.filePath, this.changes, options ?? {});
31461
- }
31462
- /**
31463
- * Copies the file to a new path and applies pending changes to the copy.
31464
- *
31465
- * If there are no pending changes, only the copy is performed.
31466
- * On dcmodify failure, the copy is cleaned up.
31467
- *
31468
- * @param outputPath - Destination filesystem path
31469
- * @param options - Timeout and abort options
31470
- * @returns A Result containing the branded output path or an error
31471
- */
31472
- async writeAs(outputPath, options) {
31473
- const outPathResult = createDicomFilePath(outputPath);
31474
- if (!outPathResult.ok) return err(outPathResult.error);
31475
- const copyResult = await copyFileSafe(this.filePath, outputPath);
31476
- if (!copyResult.ok) return err(copyResult.error);
31477
- if (this.changes.isEmpty) return ok(outPathResult.value);
31478
- const applyResult = await applyModifications(outPathResult.value, this.changes, options ?? {});
31479
- if (!applyResult.ok) {
31480
- await unlinkFile(outputPath);
31481
- return err(applyResult.error);
31482
- }
31483
- return ok(outPathResult.value);
31484
- }
31485
- /**
31486
- * Gets the file size in bytes.
31487
- *
31488
- * @returns A Result containing the size or an error
31489
- */
31490
- async fileSize() {
31491
- return statFileSize(this.filePath);
31492
- }
31493
- /**
31494
- * Deletes the file from the filesystem.
31495
- *
31496
- * @returns A Result indicating success or failure
31497
- */
31498
- async unlink() {
31499
- return unlinkFile(this.filePath);
31500
- }
31501
- };
31502
- var Dcm2xmlCharset = {
31503
- /** Use UTF-8 encoding (default). */
31504
- UTF8: "utf8",
31505
- /** Use Latin-1 encoding. */
31506
- LATIN1: "latin1",
31507
- /** Use ASCII encoding. */
31508
- ASCII: "ascii"
31509
- };
31510
- var Dcm2xmlOptionsSchema = zod.z.object({
31511
- timeoutMs: zod.z.number().int().positive().optional(),
31512
- signal: zod.z.instanceof(AbortSignal).optional(),
31513
- namespace: zod.z.boolean().optional(),
31514
- charset: zod.z.enum(["utf8", "latin1", "ascii"]).optional(),
31515
- writeBinaryData: zod.z.boolean().optional(),
31516
- encodeBinaryBase64: zod.z.boolean().optional()
31517
- }).strict().optional();
31518
- function buildArgs2(inputPath, options) {
31519
- const args = [];
31520
- if (options?.namespace === true) {
31521
- args.push("+Xn");
31522
- }
31523
- if (options?.charset === "latin1") {
31524
- args.push("+Cl");
31525
- } else if (options?.charset === "ascii") {
31526
- args.push("+Ca");
31527
- }
31528
- if (options?.writeBinaryData === true) {
31529
- args.push("+Wb");
31530
- if (options.encodeBinaryBase64 !== false) {
31531
- args.push("+Eb");
31532
- }
31533
- }
31534
- args.push(inputPath);
31535
- return args;
31536
- }
31537
- async function dcm2xml(inputPath, options) {
31538
- const validation = Dcm2xmlOptionsSchema.safeParse(options);
31539
- if (!validation.success) {
31540
- return err(new Error(`dcm2xml: invalid options: ${validation.error.message}`));
31541
- }
31542
- const binaryResult = resolveBinary("dcm2xml");
31543
- if (!binaryResult.ok) {
31544
- return err(binaryResult.error);
31545
- }
31546
- const args = buildArgs2(inputPath, options);
31547
- const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
31548
- const result = await execCommand(binaryResult.value, args, {
31549
- timeoutMs,
31550
- signal: options?.signal
31551
- });
31552
- if (!result.ok) {
31553
- return err(result.error);
31554
- }
31555
- if (result.value.exitCode !== 0) {
31556
- return err(createToolError("dcm2xml", args, result.value.exitCode, result.value.stderr));
31557
- }
31558
- return ok({ xml: result.value.stdout });
31559
- }
31560
31441
  var DcmdumpFormat = {
31561
31442
  /** Print standard DCMTK format. */
31562
31443
  STANDARD: "standard",
@@ -31571,7 +31452,7 @@ var DcmdumpOptionsSchema = zod.z.object({
31571
31452
  searchTag: zod.z.string().regex(/^\([0-9A-Fa-f]{4},[0-9A-Fa-f]{4}\)$/).optional(),
31572
31453
  printValues: zod.z.boolean().optional()
31573
31454
  }).strict().optional();
31574
- function buildArgs3(inputPath, options) {
31455
+ function buildArgs2(inputPath, options) {
31575
31456
  const args = [];
31576
31457
  if (options?.format === "short") {
31577
31458
  args.push("+L");
@@ -31580,8 +31461,8 @@ function buildArgs3(inputPath, options) {
31580
31461
  args.push("+P", "all");
31581
31462
  }
31582
31463
  if (options?.searchTag !== void 0) {
31583
- const tag = options.searchTag.replace(/[()]/g, "");
31584
- args.push("+P", tag);
31464
+ const tag2 = options.searchTag.replace(/[()]/g, "");
31465
+ args.push("+P", tag2);
31585
31466
  }
31586
31467
  if (options?.printValues === true) {
31587
31468
  args.push("+Vr");
@@ -31592,13 +31473,13 @@ function buildArgs3(inputPath, options) {
31592
31473
  async function dcmdump(inputPath, options) {
31593
31474
  const validation = DcmdumpOptionsSchema.safeParse(options);
31594
31475
  if (!validation.success) {
31595
- return err(new Error(`dcmdump: invalid options: ${validation.error.message}`));
31476
+ return err(createValidationError("dcmdump", validation.error));
31596
31477
  }
31597
31478
  const binaryResult = resolveBinary("dcmdump");
31598
31479
  if (!binaryResult.ok) {
31599
31480
  return err(binaryResult.error);
31600
31481
  }
31601
- const args = buildArgs3(inputPath, options);
31482
+ const args = buildArgs2(inputPath, options);
31602
31483
  const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
31603
31484
  const result = await execCommand(binaryResult.value, args, {
31604
31485
  timeoutMs,
@@ -31637,7 +31518,7 @@ var DcmconvOptionsSchema = zod.z.object({
31637
31518
  async function dcmconv(inputPath, outputPath, options) {
31638
31519
  const validation = DcmconvOptionsSchema.safeParse(options);
31639
31520
  if (!validation.success) {
31640
- return err(new Error(`dcmconv: invalid options: ${validation.error.message}`));
31521
+ return err(createValidationError("dcmconv", validation.error));
31641
31522
  }
31642
31523
  const binaryResult = resolveBinary("dcmconv");
31643
31524
  if (!binaryResult.ok) {
@@ -31657,6 +31538,70 @@ async function dcmconv(inputPath, outputPath, options) {
31657
31538
  }
31658
31539
  return ok({ outputPath });
31659
31540
  }
31541
+ var TAG_OR_PATH_PATTERN = /^\([0-9A-Fa-f]{4},[0-9A-Fa-f]{4}\)(\[\d+\](\.\([0-9A-Fa-f]{4},[0-9A-Fa-f]{4}\)(\[\d+\])?)*)?$/;
31542
+ var TagModificationSchema = zod.z.object({
31543
+ tag: zod.z.string().regex(TAG_OR_PATH_PATTERN),
31544
+ value: zod.z.string()
31545
+ });
31546
+ var DcmodifyOptionsSchema = zod.z.object({
31547
+ timeoutMs: zod.z.number().int().positive().optional(),
31548
+ signal: zod.z.instanceof(AbortSignal).optional(),
31549
+ modifications: zod.z.array(TagModificationSchema).optional().default([]),
31550
+ erasures: zod.z.array(zod.z.string()).optional(),
31551
+ erasePrivateTags: zod.z.boolean().optional(),
31552
+ noBackup: zod.z.boolean().optional(),
31553
+ insertIfMissing: zod.z.boolean().optional(),
31554
+ ignoreMissingTags: zod.z.boolean().optional()
31555
+ }).strict().refine((data) => data.modifications.length > 0 || data.erasures !== void 0 && data.erasures.length > 0 || data.erasePrivateTags === true, {
31556
+ message: "At least one of modifications, erasures, or erasePrivateTags is required"
31557
+ });
31558
+ function buildArgs3(inputPath, options) {
31559
+ const args = [];
31560
+ if (options.noBackup !== false) {
31561
+ args.push("-nb");
31562
+ }
31563
+ if (options.ignoreMissingTags === true) {
31564
+ args.push("-imt");
31565
+ }
31566
+ const flag = options.insertIfMissing === true ? "-i" : "-m";
31567
+ const modifications = options.modifications ?? [];
31568
+ for (const mod of modifications) {
31569
+ args.push(flag, `${mod.tag}=${mod.value}`);
31570
+ }
31571
+ if (options.erasures !== void 0) {
31572
+ for (const erasure of options.erasures) {
31573
+ args.push("-e", erasure);
31574
+ }
31575
+ }
31576
+ if (options.erasePrivateTags === true) {
31577
+ args.push("-ep");
31578
+ }
31579
+ args.push(inputPath);
31580
+ return args;
31581
+ }
31582
+ async function dcmodify(inputPath, options) {
31583
+ const validation = DcmodifyOptionsSchema.safeParse(options);
31584
+ if (!validation.success) {
31585
+ return err(createValidationError("dcmodify", validation.error));
31586
+ }
31587
+ const binaryResult = resolveBinary("dcmodify");
31588
+ if (!binaryResult.ok) {
31589
+ return err(binaryResult.error);
31590
+ }
31591
+ const args = buildArgs3(inputPath, options);
31592
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
31593
+ const result = await spawnCommand(binaryResult.value, args, {
31594
+ timeoutMs,
31595
+ signal: options.signal
31596
+ });
31597
+ if (!result.ok) {
31598
+ return err(result.error);
31599
+ }
31600
+ if (result.value.exitCode !== 0) {
31601
+ return err(createToolError("dcmodify", args, result.value.exitCode, result.value.stderr));
31602
+ }
31603
+ return ok({ filePath: inputPath });
31604
+ }
31660
31605
  var DcmftestOptionsSchema = zod.z.object({
31661
31606
  timeoutMs: zod.z.number().int().positive().optional(),
31662
31607
  signal: zod.z.instanceof(AbortSignal).optional()
@@ -31664,7 +31609,7 @@ var DcmftestOptionsSchema = zod.z.object({
31664
31609
  async function dcmftest(inputPath, options) {
31665
31610
  const validation = DcmftestOptionsSchema.safeParse(options);
31666
31611
  if (!validation.success) {
31667
- return err(new Error(`dcmftest: invalid options: ${validation.error.message}`));
31612
+ return err(createValidationError("dcmftest", validation.error));
31668
31613
  }
31669
31614
  const binaryResult = resolveBinary("dcmftest");
31670
31615
  if (!binaryResult.ok) {
@@ -31719,7 +31664,7 @@ function buildArgs4(options) {
31719
31664
  async function dcmgpdir(options) {
31720
31665
  const validation = DcmgpdirOptionsSchema.safeParse(options);
31721
31666
  if (!validation.success) {
31722
- return err(new Error(`dcmgpdir: invalid options: ${validation.error.message}`));
31667
+ return err(createValidationError("dcmgpdir", validation.error));
31723
31668
  }
31724
31669
  const binaryResult = resolveBinary("dcmgpdir");
31725
31670
  if (!binaryResult.ok) {
@@ -31778,7 +31723,7 @@ function buildArgs5(options) {
31778
31723
  async function dcmmkdir(options) {
31779
31724
  const validation = DcmmkdirOptionsSchema.safeParse(options);
31780
31725
  if (!validation.success) {
31781
- return err(new Error(`dcmmkdir: invalid options: ${validation.error.message}`));
31726
+ return err(createValidationError("dcmmkdir", validation.error));
31782
31727
  }
31783
31728
  const binaryResult = resolveBinary("dcmmkdir");
31784
31729
  if (!binaryResult.ok) {
@@ -31826,7 +31771,7 @@ function buildArgs6(options) {
31826
31771
  async function dcmqridx(options) {
31827
31772
  const validation = DcmqridxOptionsSchema.safeParse(options);
31828
31773
  if (!validation.success) {
31829
- return err(new Error(`dcmqridx: invalid options: ${validation.error.message}`));
31774
+ return err(createValidationError("dcmqridx", validation.error));
31830
31775
  }
31831
31776
  const binaryResult = resolveBinary("dcmqridx");
31832
31777
  if (!binaryResult.ok) {
@@ -31869,7 +31814,7 @@ function buildArgs7(inputPath, outputPath, options) {
31869
31814
  async function xml2dcm(inputPath, outputPath, options) {
31870
31815
  const validation = Xml2dcmOptionsSchema.safeParse(options);
31871
31816
  if (!validation.success) {
31872
- return err(new Error(`xml2dcm: invalid options: ${validation.error.message}`));
31817
+ return err(createValidationError("xml2dcm", validation.error));
31873
31818
  }
31874
31819
  const binaryResult = resolveBinary("xml2dcm");
31875
31820
  if (!binaryResult.ok) {
@@ -31896,7 +31841,7 @@ var Json2dcmOptionsSchema = zod.z.object({
31896
31841
  async function json2dcm(inputPath, outputPath, options) {
31897
31842
  const validation = Json2dcmOptionsSchema.safeParse(options);
31898
31843
  if (!validation.success) {
31899
- return err(new Error(`json2dcm: invalid options: ${validation.error.message}`));
31844
+ return err(createValidationError("json2dcm", validation.error));
31900
31845
  }
31901
31846
  const binaryResult = resolveBinary("json2dcm");
31902
31847
  if (!binaryResult.ok) {
@@ -31936,7 +31881,7 @@ function buildArgs8(inputPath, outputPath, options) {
31936
31881
  async function dump2dcm(inputPath, outputPath, options) {
31937
31882
  const validation = Dump2dcmOptionsSchema.safeParse(options);
31938
31883
  if (!validation.success) {
31939
- return err(new Error(`dump2dcm: invalid options: ${validation.error.message}`));
31884
+ return err(createValidationError("dump2dcm", validation.error));
31940
31885
  }
31941
31886
  const binaryResult = resolveBinary("dump2dcm");
31942
31887
  if (!binaryResult.ok) {
@@ -31986,7 +31931,7 @@ function buildArgs9(inputPath, outputPath, options) {
31986
31931
  async function img2dcm(inputPath, outputPath, options) {
31987
31932
  const validation = Img2dcmOptionsSchema.safeParse(options);
31988
31933
  if (!validation.success) {
31989
- return err(new Error(`img2dcm: invalid options: ${validation.error.message}`));
31934
+ return err(createValidationError("img2dcm", validation.error));
31990
31935
  }
31991
31936
  const binaryResult = resolveBinary("img2dcm");
31992
31937
  if (!binaryResult.ok) {
@@ -32013,7 +31958,7 @@ var Pdf2dcmOptionsSchema = zod.z.object({
32013
31958
  async function pdf2dcm(inputPath, outputPath, options) {
32014
31959
  const validation = Pdf2dcmOptionsSchema.safeParse(options);
32015
31960
  if (!validation.success) {
32016
- return err(new Error(`pdf2dcm: invalid options: ${validation.error.message}`));
31961
+ return err(createValidationError("pdf2dcm", validation.error));
32017
31962
  }
32018
31963
  const binaryResult = resolveBinary("pdf2dcm");
32019
31964
  if (!binaryResult.ok) {
@@ -32040,7 +31985,7 @@ var Dcm2pdfOptionsSchema = zod.z.object({
32040
31985
  async function dcm2pdf(inputPath, outputPath, options) {
32041
31986
  const validation = Dcm2pdfOptionsSchema.safeParse(options);
32042
31987
  if (!validation.success) {
32043
- return err(new Error(`dcm2pdf: invalid options: ${validation.error.message}`));
31988
+ return err(createValidationError("dcm2pdf", validation.error));
32044
31989
  }
32045
31990
  const binaryResult = resolveBinary("dcm2pdf");
32046
31991
  if (!binaryResult.ok) {
@@ -32067,7 +32012,7 @@ var Cda2dcmOptionsSchema = zod.z.object({
32067
32012
  async function cda2dcm(inputPath, outputPath, options) {
32068
32013
  const validation = Cda2dcmOptionsSchema.safeParse(options);
32069
32014
  if (!validation.success) {
32070
- return err(new Error(`cda2dcm: invalid options: ${validation.error.message}`));
32015
+ return err(createValidationError("cda2dcm", validation.error));
32071
32016
  }
32072
32017
  const binaryResult = resolveBinary("cda2dcm");
32073
32018
  if (!binaryResult.ok) {
@@ -32094,7 +32039,7 @@ var Dcm2cdaOptionsSchema = zod.z.object({
32094
32039
  async function dcm2cda(inputPath, outputPath, options) {
32095
32040
  const validation = Dcm2cdaOptionsSchema.safeParse(options);
32096
32041
  if (!validation.success) {
32097
- return err(new Error(`dcm2cda: invalid options: ${validation.error.message}`));
32042
+ return err(createValidationError("dcm2cda", validation.error));
32098
32043
  }
32099
32044
  const binaryResult = resolveBinary("dcm2cda");
32100
32045
  if (!binaryResult.ok) {
@@ -32121,7 +32066,7 @@ var Stl2dcmOptionsSchema = zod.z.object({
32121
32066
  async function stl2dcm(inputPath, outputPath, options) {
32122
32067
  const validation = Stl2dcmOptionsSchema.safeParse(options);
32123
32068
  if (!validation.success) {
32124
- return err(new Error(`stl2dcm: invalid options: ${validation.error.message}`));
32069
+ return err(createValidationError("stl2dcm", validation.error));
32125
32070
  }
32126
32071
  const binaryResult = resolveBinary("stl2dcm");
32127
32072
  if (!binaryResult.ok) {
@@ -32157,7 +32102,7 @@ function buildArgs10(inputPath, outputPath, options) {
32157
32102
  async function dcmcrle(inputPath, outputPath, options) {
32158
32103
  const validation = DcmcrleOptionsSchema.safeParse(options);
32159
32104
  if (!validation.success) {
32160
- return err(new Error(`dcmcrle: invalid options: ${validation.error.message}`));
32105
+ return err(createValidationError("dcmcrle", validation.error));
32161
32106
  }
32162
32107
  const binaryResult = resolveBinary("dcmcrle");
32163
32108
  if (!binaryResult.ok) {
@@ -32193,7 +32138,7 @@ function buildArgs11(inputPath, outputPath, options) {
32193
32138
  async function dcmdrle(inputPath, outputPath, options) {
32194
32139
  const validation = DcmdrleOptionsSchema.safeParse(options);
32195
32140
  if (!validation.success) {
32196
- return err(new Error(`dcmdrle: invalid options: ${validation.error.message}`));
32141
+ return err(createValidationError("dcmdrle", validation.error));
32197
32142
  }
32198
32143
  const binaryResult = resolveBinary("dcmdrle");
32199
32144
  if (!binaryResult.ok) {
@@ -32229,7 +32174,7 @@ function buildArgs12(inputPath, outputPath, options) {
32229
32174
  async function dcmencap(inputPath, outputPath, options) {
32230
32175
  const validation = DcmencapOptionsSchema.safeParse(options);
32231
32176
  if (!validation.success) {
32232
- return err(new Error(`dcmencap: invalid options: ${validation.error.message}`));
32177
+ return err(createValidationError("dcmencap", validation.error));
32233
32178
  }
32234
32179
  const binaryResult = resolveBinary("dcmencap");
32235
32180
  if (!binaryResult.ok) {
@@ -32256,7 +32201,7 @@ var DcmdecapOptionsSchema = zod.z.object({
32256
32201
  async function dcmdecap(inputPath, outputPath, options) {
32257
32202
  const validation = DcmdecapOptionsSchema.safeParse(options);
32258
32203
  if (!validation.success) {
32259
- return err(new Error(`dcmdecap: invalid options: ${validation.error.message}`));
32204
+ return err(createValidationError("dcmdecap", validation.error));
32260
32205
  }
32261
32206
  const binaryResult = resolveBinary("dcmdecap");
32262
32207
  if (!binaryResult.ok) {
@@ -32296,7 +32241,7 @@ function buildArgs13(inputPath, outputPath, options) {
32296
32241
  async function dcmcjpeg(inputPath, outputPath, options) {
32297
32242
  const validation = DcmcjpegOptionsSchema.safeParse(options);
32298
32243
  if (!validation.success) {
32299
- return err(new Error(`dcmcjpeg: invalid options: ${validation.error.message}`));
32244
+ return err(createValidationError("dcmcjpeg", validation.error));
32300
32245
  }
32301
32246
  const binaryResult = resolveBinary("dcmcjpeg");
32302
32247
  if (!binaryResult.ok) {
@@ -32345,7 +32290,7 @@ function buildArgs14(inputPath, outputPath, options) {
32345
32290
  async function dcmdjpeg(inputPath, outputPath, options) {
32346
32291
  const validation = DcmdjpegOptionsSchema.safeParse(options);
32347
32292
  if (!validation.success) {
32348
- return err(new Error(`dcmdjpeg: invalid options: ${validation.error.message}`));
32293
+ return err(createValidationError("dcmdjpeg", validation.error));
32349
32294
  }
32350
32295
  const binaryResult = resolveBinary("dcmdjpeg");
32351
32296
  if (!binaryResult.ok) {
@@ -32387,7 +32332,7 @@ function buildArgs15(inputPath, outputPath, options) {
32387
32332
  async function dcmcjpls(inputPath, outputPath, options) {
32388
32333
  const validation = DcmcjplsOptionsSchema.safeParse(options);
32389
32334
  if (!validation.success) {
32390
- return err(new Error(`dcmcjpls: invalid options: ${validation.error.message}`));
32335
+ return err(createValidationError("dcmcjpls", validation.error));
32391
32336
  }
32392
32337
  const binaryResult = resolveBinary("dcmcjpls");
32393
32338
  if (!binaryResult.ok) {
@@ -32436,7 +32381,7 @@ function buildArgs16(inputPath, outputPath, options) {
32436
32381
  async function dcmdjpls(inputPath, outputPath, options) {
32437
32382
  const validation = DcmdjplsOptionsSchema.safeParse(options);
32438
32383
  if (!validation.success) {
32439
- return err(new Error(`dcmdjpls: invalid options: ${validation.error.message}`));
32384
+ return err(createValidationError("dcmdjpls", validation.error));
32440
32385
  }
32441
32386
  const binaryResult = resolveBinary("dcmdjpls");
32442
32387
  if (!binaryResult.ok) {
@@ -32495,7 +32440,7 @@ function buildArgs17(inputPath, outputPath, options) {
32495
32440
  async function dcmj2pnm(inputPath, outputPath, options) {
32496
32441
  const validation = Dcmj2pnmOptionsSchema.safeParse(options);
32497
32442
  if (!validation.success) {
32498
- return err(new Error(`dcmj2pnm: invalid options: ${validation.error.message}`));
32443
+ return err(createValidationError("dcmj2pnm", validation.error));
32499
32444
  }
32500
32445
  const binaryResult = resolveBinary("dcmj2pnm");
32501
32446
  if (!binaryResult.ok) {
@@ -32551,7 +32496,7 @@ function buildArgs18(inputPath, outputPath, options) {
32551
32496
  async function dcm2pnm(inputPath, outputPath, options) {
32552
32497
  const validation = Dcm2pnmOptionsSchema.safeParse(options);
32553
32498
  if (!validation.success) {
32554
- return err(new Error(`dcm2pnm: invalid options: ${validation.error.message}`));
32499
+ return err(createValidationError("dcm2pnm", validation.error));
32555
32500
  }
32556
32501
  const binaryResult = resolveBinary("dcm2pnm");
32557
32502
  if (!binaryResult.ok) {
@@ -32599,7 +32544,7 @@ function buildArgs19(inputPath, outputPath, options) {
32599
32544
  async function dcmscale(inputPath, outputPath, options) {
32600
32545
  const validation = DcmscaleOptionsSchema.safeParse(options);
32601
32546
  if (!validation.success) {
32602
- return err(new Error(`dcmscale: invalid options: ${validation.error.message}`));
32547
+ return err(createValidationError("dcmscale", validation.error));
32603
32548
  }
32604
32549
  const binaryResult = resolveBinary("dcmscale");
32605
32550
  if (!binaryResult.ok) {
@@ -32639,7 +32584,7 @@ function buildArgs20(inputPath, outputPath, options) {
32639
32584
  async function dcmquant(inputPath, outputPath, options) {
32640
32585
  const validation = DcmquantOptionsSchema.safeParse(options);
32641
32586
  if (!validation.success) {
32642
- return err(new Error(`dcmquant: invalid options: ${validation.error.message}`));
32587
+ return err(createValidationError("dcmquant", validation.error));
32643
32588
  }
32644
32589
  const binaryResult = resolveBinary("dcmquant");
32645
32590
  if (!binaryResult.ok) {
@@ -32686,7 +32631,7 @@ function buildArgs21(options) {
32686
32631
  async function dcmdspfn(options) {
32687
32632
  const validation = DcmdspfnOptionsSchema.safeParse(options);
32688
32633
  if (!validation.success) {
32689
- return err(new Error(`dcmdspfn: invalid options: ${validation.error.message}`));
32634
+ return err(createValidationError("dcmdspfn", validation.error));
32690
32635
  }
32691
32636
  const binaryResult = resolveBinary("dcmdspfn");
32692
32637
  if (!binaryResult.ok) {
@@ -32713,7 +32658,7 @@ var Dcod2lumOptionsSchema = zod.z.object({
32713
32658
  async function dcod2lum(inputPath, outputPath, options) {
32714
32659
  const validation = Dcod2lumOptionsSchema.safeParse(options);
32715
32660
  if (!validation.success) {
32716
- return err(new Error(`dcod2lum: invalid options: ${validation.error.message}`));
32661
+ return err(createValidationError("dcod2lum", validation.error));
32717
32662
  }
32718
32663
  const binaryResult = resolveBinary("dcod2lum");
32719
32664
  if (!binaryResult.ok) {
@@ -32749,7 +32694,7 @@ function buildArgs22(inputPath, outputPath, options) {
32749
32694
  async function dconvlum(inputPath, outputPath, options) {
32750
32695
  const validation = DconvlumOptionsSchema.safeParse(options);
32751
32696
  if (!validation.success) {
32752
- return err(new Error(`dconvlum: invalid options: ${validation.error.message}`));
32697
+ return err(createValidationError("dconvlum", validation.error));
32753
32698
  }
32754
32699
  const binaryResult = resolveBinary("dconvlum");
32755
32700
  if (!binaryResult.ok) {
@@ -32839,7 +32784,7 @@ function buildArgs24(options) {
32839
32784
  async function dcmsend(options) {
32840
32785
  const validation = DcmsendOptionsSchema.safeParse(options);
32841
32786
  if (!validation.success) {
32842
- return err(new Error(`dcmsend: invalid options: ${validation.error.message}`));
32787
+ return err(createValidationError("dcmsend", validation.error));
32843
32788
  }
32844
32789
  const binaryResult = resolveBinary("dcmsend");
32845
32790
  if (!binaryResult.ok) {
@@ -32859,6 +32804,32 @@ async function dcmsend(options) {
32859
32804
  }
32860
32805
  return ok({ success: true, stderr: result.value.stderr });
32861
32806
  }
32807
+ var ProposedTransferSyntax = {
32808
+ UNCOMPRESSED: "uncompressed",
32809
+ LITTLE_ENDIAN: "littleEndian",
32810
+ BIG_ENDIAN: "bigEndian",
32811
+ IMPLICIT_VR: "implicitVR",
32812
+ JPEG_LOSSLESS: "jpegLossless",
32813
+ JPEG_8BIT: "jpeg8Bit",
32814
+ JPEG_12BIT: "jpeg12Bit",
32815
+ J2K_LOSSLESS: "j2kLossless",
32816
+ J2K_LOSSY: "j2kLossy",
32817
+ JLS_LOSSLESS: "jlsLossless",
32818
+ JLS_LOSSY: "jlsLossy"
32819
+ };
32820
+ var PROPOSED_TS_FLAG_MAP = {
32821
+ [ProposedTransferSyntax.UNCOMPRESSED]: "-x=",
32822
+ [ProposedTransferSyntax.LITTLE_ENDIAN]: "-xe",
32823
+ [ProposedTransferSyntax.BIG_ENDIAN]: "-xb",
32824
+ [ProposedTransferSyntax.IMPLICIT_VR]: "-xi",
32825
+ [ProposedTransferSyntax.JPEG_LOSSLESS]: "-xs",
32826
+ [ProposedTransferSyntax.JPEG_8BIT]: "-xy",
32827
+ [ProposedTransferSyntax.JPEG_12BIT]: "-xx",
32828
+ [ProposedTransferSyntax.J2K_LOSSLESS]: "-xv",
32829
+ [ProposedTransferSyntax.J2K_LOSSY]: "-xw",
32830
+ [ProposedTransferSyntax.JLS_LOSSLESS]: "-xt",
32831
+ [ProposedTransferSyntax.JLS_LOSSY]: "-xu"
32832
+ };
32862
32833
  var StorescuOptionsSchema = zod.z.object({
32863
32834
  timeoutMs: zod.z.number().int().positive().optional(),
32864
32835
  signal: zod.z.instanceof(AbortSignal).optional(),
@@ -32868,7 +32839,20 @@ var StorescuOptionsSchema = zod.z.object({
32868
32839
  callingAETitle: zod.z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
32869
32840
  calledAETitle: zod.z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
32870
32841
  scanDirectories: zod.z.boolean().optional(),
32871
- recurse: zod.z.boolean().optional()
32842
+ recurse: zod.z.boolean().optional(),
32843
+ proposedTransferSyntax: zod.z.enum([
32844
+ "uncompressed",
32845
+ "littleEndian",
32846
+ "bigEndian",
32847
+ "implicitVR",
32848
+ "jpegLossless",
32849
+ "jpeg8Bit",
32850
+ "jpeg12Bit",
32851
+ "j2kLossless",
32852
+ "j2kLossy",
32853
+ "jlsLossless",
32854
+ "jlsLossy"
32855
+ ]).optional()
32872
32856
  }).strict();
32873
32857
  function buildArgs25(options) {
32874
32858
  const args = [];
@@ -32884,6 +32868,9 @@ function buildArgs25(options) {
32884
32868
  if (options.recurse === true) {
32885
32869
  args.push("+r");
32886
32870
  }
32871
+ if (options.proposedTransferSyntax !== void 0) {
32872
+ args.push(PROPOSED_TS_FLAG_MAP[options.proposedTransferSyntax]);
32873
+ }
32887
32874
  args.push(options.host, String(options.port));
32888
32875
  args.push(...options.files);
32889
32876
  return args;
@@ -32891,7 +32878,7 @@ function buildArgs25(options) {
32891
32878
  async function storescu(options) {
32892
32879
  const validation = StorescuOptionsSchema.safeParse(options);
32893
32880
  if (!validation.success) {
32894
- return err(new Error(`storescu: invalid options: ${validation.error.message}`));
32881
+ return err(createValidationError("storescu", validation.error));
32895
32882
  }
32896
32883
  const binaryResult = resolveBinary("storescu");
32897
32884
  if (!binaryResult.ok) {
@@ -33031,7 +33018,7 @@ function buildArgs27(options) {
33031
33018
  async function movescu(options) {
33032
33019
  const validation = MovescuOptionsSchema.safeParse(options);
33033
33020
  if (!validation.success) {
33034
- return err(new Error(`movescu: invalid options: ${validation.error.message}`));
33021
+ return err(createValidationError("movescu", validation.error));
33035
33022
  }
33036
33023
  const binaryResult = resolveBinary("movescu");
33037
33024
  if (!binaryResult.ok) {
@@ -33095,7 +33082,7 @@ function buildArgs28(options) {
33095
33082
  async function getscu(options) {
33096
33083
  const validation = GetscuOptionsSchema.safeParse(options);
33097
33084
  if (!validation.success) {
33098
- return err(new Error(`getscu: invalid options: ${validation.error.message}`));
33085
+ return err(createValidationError("getscu", validation.error));
33099
33086
  }
33100
33087
  const binaryResult = resolveBinary("getscu");
33101
33088
  if (!binaryResult.ok) {
@@ -33137,7 +33124,7 @@ function buildArgs29(options) {
33137
33124
  async function termscu(options) {
33138
33125
  const validation = TermscuOptionsSchema.safeParse(options);
33139
33126
  if (!validation.success) {
33140
- return err(new Error(`termscu: invalid options: ${validation.error.message}`));
33127
+ return err(createValidationError("termscu", validation.error));
33141
33128
  }
33142
33129
  const binaryResult = resolveBinary("termscu");
33143
33130
  if (!binaryResult.ok) {
@@ -33181,7 +33168,7 @@ function buildArgs30(inputPath, options) {
33181
33168
  async function dsrdump(inputPath, options) {
33182
33169
  const validation = DsrdumpOptionsSchema.safeParse(options);
33183
33170
  if (!validation.success) {
33184
- return err(new Error(`dsrdump: invalid options: ${validation.error.message}`));
33171
+ return err(createValidationError("dsrdump", validation.error));
33185
33172
  }
33186
33173
  const binaryResult = resolveBinary("dsrdump");
33187
33174
  if (!binaryResult.ok) {
@@ -33221,7 +33208,7 @@ function buildArgs31(inputPath, options) {
33221
33208
  async function dsr2xml(inputPath, options) {
33222
33209
  const validation = Dsr2xmlOptionsSchema.safeParse(options);
33223
33210
  if (!validation.success) {
33224
- return err(new Error(`dsr2xml: invalid options: ${validation.error.message}`));
33211
+ return err(createValidationError("dsr2xml", validation.error));
33225
33212
  }
33226
33213
  const binaryResult = resolveBinary("dsr2xml");
33227
33214
  if (!binaryResult.ok) {
@@ -33261,7 +33248,7 @@ function buildArgs32(inputPath, outputPath, options) {
33261
33248
  async function xml2dsr(inputPath, outputPath, options) {
33262
33249
  const validation = Xml2dsrOptionsSchema.safeParse(options);
33263
33250
  if (!validation.success) {
33264
- return err(new Error(`xml2dsr: invalid options: ${validation.error.message}`));
33251
+ return err(createValidationError("xml2dsr", validation.error));
33265
33252
  }
33266
33253
  const binaryResult = resolveBinary("xml2dsr");
33267
33254
  if (!binaryResult.ok) {
@@ -33297,7 +33284,7 @@ function buildArgs33(inputPath, options) {
33297
33284
  async function drtdump(inputPath, options) {
33298
33285
  const validation = DrtdumpOptionsSchema.safeParse(options);
33299
33286
  if (!validation.success) {
33300
- return err(new Error(`drtdump: invalid options: ${validation.error.message}`));
33287
+ return err(createValidationError("drtdump", validation.error));
33301
33288
  }
33302
33289
  const binaryResult = resolveBinary("drtdump");
33303
33290
  if (!binaryResult.ok) {
@@ -33324,7 +33311,7 @@ var DcmpsmkOptionsSchema = zod.z.object({
33324
33311
  async function dcmpsmk(inputPath, outputPath, options) {
33325
33312
  const validation = DcmpsmkOptionsSchema.safeParse(options);
33326
33313
  if (!validation.success) {
33327
- return err(new Error(`dcmpsmk: invalid options: ${validation.error.message}`));
33314
+ return err(createValidationError("dcmpsmk", validation.error));
33328
33315
  }
33329
33316
  const binaryResult = resolveBinary("dcmpsmk");
33330
33317
  if (!binaryResult.ok) {
@@ -33351,7 +33338,7 @@ var DcmpschkOptionsSchema = zod.z.object({
33351
33338
  async function dcmpschk(inputPath, options) {
33352
33339
  const validation = DcmpschkOptionsSchema.safeParse(options);
33353
33340
  if (!validation.success) {
33354
- return err(new Error(`dcmpschk: invalid options: ${validation.error.message}`));
33341
+ return err(createValidationError("dcmpschk", validation.error));
33355
33342
  }
33356
33343
  const binaryResult = resolveBinary("dcmpschk");
33357
33344
  if (!binaryResult.ok) {
@@ -33397,7 +33384,7 @@ function buildArgs34(options) {
33397
33384
  async function dcmprscu(options) {
33398
33385
  const validation = DcmprscuOptionsSchema.safeParse(options);
33399
33386
  if (!validation.success) {
33400
- return err(new Error(`dcmprscu: invalid options: ${validation.error.message}`));
33387
+ return err(createValidationError("dcmprscu", validation.error));
33401
33388
  }
33402
33389
  const binaryResult = resolveBinary("dcmprscu");
33403
33390
  if (!binaryResult.ok) {
@@ -33433,7 +33420,7 @@ function buildArgs35(inputPath, options) {
33433
33420
  async function dcmpsprt(inputPath, options) {
33434
33421
  const validation = DcmpsprtOptionsSchema.safeParse(options);
33435
33422
  if (!validation.success) {
33436
- return err(new Error(`dcmpsprt: invalid options: ${validation.error.message}`));
33423
+ return err(createValidationError("dcmpsprt", validation.error));
33437
33424
  }
33438
33425
  const binaryResult = resolveBinary("dcmpsprt");
33439
33426
  if (!binaryResult.ok) {
@@ -33473,7 +33460,7 @@ function buildArgs36(inputPath, outputPath, options) {
33473
33460
  async function dcmp2pgm(inputPath, outputPath, options) {
33474
33461
  const validation = Dcmp2pgmOptionsSchema.safeParse(options);
33475
33462
  if (!validation.success) {
33476
- return err(new Error(`dcmp2pgm: invalid options: ${validation.error.message}`));
33463
+ return err(createValidationError("dcmp2pgm", validation.error));
33477
33464
  }
33478
33465
  const binaryResult = resolveBinary("dcmp2pgm");
33479
33466
  if (!binaryResult.ok) {
@@ -33500,7 +33487,7 @@ var DcmmkcrvOptionsSchema = zod.z.object({
33500
33487
  async function dcmmkcrv(inputPath, outputPath, options) {
33501
33488
  const validation = DcmmkcrvOptionsSchema.safeParse(options);
33502
33489
  if (!validation.success) {
33503
- return err(new Error(`dcmmkcrv: invalid options: ${validation.error.message}`));
33490
+ return err(createValidationError("dcmmkcrv", validation.error));
33504
33491
  }
33505
33492
  const binaryResult = resolveBinary("dcmmkcrv");
33506
33493
  if (!binaryResult.ok) {
@@ -33561,7 +33548,7 @@ function buildArgs37(outputPath, options) {
33561
33548
  async function dcmmklut(outputPath, options) {
33562
33549
  const validation = DcmmklutOptionsSchema.safeParse(options);
33563
33550
  if (!validation.success) {
33564
- return err(new Error(`dcmmklut: invalid options: ${validation.error.message}`));
33551
+ return err(createValidationError("dcmmklut", validation.error));
33565
33552
  }
33566
33553
  const binaryResult = resolveBinary("dcmmklut");
33567
33554
  if (!binaryResult.ok) {
@@ -33581,6 +33568,363 @@ async function dcmmklut(outputPath, options) {
33581
33568
  }
33582
33569
  return ok({ outputPath });
33583
33570
  }
33571
+ async function applyModifications(filePath, changeset, options) {
33572
+ const modifications = changeset.toModifications();
33573
+ const erasures = changeset.toErasureArgs();
33574
+ const hasErasures = erasures.length > 0 || changeset.erasePrivate;
33575
+ const result = await dcmodify(filePath, {
33576
+ modifications: modifications.length > 0 ? modifications : void 0,
33577
+ erasures: erasures.length > 0 ? erasures : void 0,
33578
+ erasePrivateTags: changeset.erasePrivate || void 0,
33579
+ insertIfMissing: true,
33580
+ ignoreMissingTags: hasErasures || void 0,
33581
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
33582
+ signal: options.signal
33583
+ });
33584
+ if (!result.ok) return err(result.error);
33585
+ return ok(void 0);
33586
+ }
33587
+ async function copyFileSafe(source, dest) {
33588
+ return stderrLib.tryCatch(
33589
+ () => fs.copyFile(source, dest),
33590
+ (e) => new Error(`Failed to copy file: ${e.message}`)
33591
+ );
33592
+ }
33593
+ async function statFileSize(path2) {
33594
+ return stderrLib.tryCatch(
33595
+ async () => (await fs.stat(path2)).size,
33596
+ (e) => new Error(`Failed to stat file: ${e.message}`)
33597
+ );
33598
+ }
33599
+ async function unlinkFile(path2) {
33600
+ return stderrLib.tryCatch(
33601
+ () => fs.unlink(path2),
33602
+ (e) => new Error(`Failed to delete file: ${e.message}`)
33603
+ );
33604
+ }
33605
+
33606
+ // src/dicom/DicomInstance.ts
33607
+ var DicomInstance = class _DicomInstance {
33608
+ constructor(dataset, changes, filePath, metadata) {
33609
+ __publicField(this, "dicomDataset");
33610
+ __publicField(this, "changeSet");
33611
+ __publicField(this, "filepath");
33612
+ __publicField(this, "meta");
33613
+ this.dicomDataset = dataset;
33614
+ this.changeSet = changes;
33615
+ this.filepath = filePath;
33616
+ this.meta = metadata;
33617
+ }
33618
+ // -----------------------------------------------------------------------
33619
+ // Factories
33620
+ // -----------------------------------------------------------------------
33621
+ /**
33622
+ * Opens a DICOM file and creates a DicomInstance.
33623
+ *
33624
+ * @param path - Filesystem path to the DICOM file
33625
+ * @param options - Timeout and abort options
33626
+ * @returns A Result containing the DicomInstance or an error
33627
+ */
33628
+ static async open(path2, options) {
33629
+ const filePathResult = createDicomFilePath(path2);
33630
+ if (!filePathResult.ok) return err(filePathResult.error);
33631
+ const jsonResult = await dcm2json(path2, {
33632
+ timeoutMs: options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
33633
+ signal: options?.signal
33634
+ });
33635
+ if (!jsonResult.ok) return err(jsonResult.error);
33636
+ const datasetResult = DicomDataset.fromJson(jsonResult.value.data);
33637
+ if (!datasetResult.ok) return err(datasetResult.error);
33638
+ return ok(new _DicomInstance(datasetResult.value, ChangeSet.empty(), filePathResult.value, /* @__PURE__ */ new Map()));
33639
+ }
33640
+ /**
33641
+ * Creates a DicomInstance from an existing DicomDataset.
33642
+ *
33643
+ * @param dataset - The DicomDataset to wrap
33644
+ * @param filePath - Optional file path (e.g. if the dataset came from a file)
33645
+ * @returns A Result containing the DicomInstance
33646
+ */
33647
+ static fromDataset(dataset, filePath) {
33648
+ let fp;
33649
+ if (filePath !== void 0) {
33650
+ const fpResult = createDicomFilePath(filePath);
33651
+ if (!fpResult.ok) return err(fpResult.error);
33652
+ fp = fpResult.value;
33653
+ }
33654
+ return ok(new _DicomInstance(dataset, ChangeSet.empty(), fp, /* @__PURE__ */ new Map()));
33655
+ }
33656
+ // -----------------------------------------------------------------------
33657
+ // Read accessors (delegate to DicomDataset)
33658
+ // -----------------------------------------------------------------------
33659
+ /** The underlying immutable DICOM dataset. */
33660
+ get dataset() {
33661
+ return this.dicomDataset;
33662
+ }
33663
+ /** The pending change set. */
33664
+ get changes() {
33665
+ return this.changeSet;
33666
+ }
33667
+ /** Whether there are unsaved changes. */
33668
+ get hasUnsavedChanges() {
33669
+ return !this.changeSet.isEmpty;
33670
+ }
33671
+ /** The file path, or undefined if this instance has no associated file. */
33672
+ get filePath() {
33673
+ return this.filepath;
33674
+ }
33675
+ /** Patient's Name (0010,0010). */
33676
+ get patientName() {
33677
+ return this.dicomDataset.patientName;
33678
+ }
33679
+ /** Patient ID (0010,0020). */
33680
+ get patientID() {
33681
+ return this.dicomDataset.patientID;
33682
+ }
33683
+ /** Study Date (0008,0020). */
33684
+ get studyDate() {
33685
+ return this.dicomDataset.studyDate;
33686
+ }
33687
+ /** Modality (0008,0060). */
33688
+ get modality() {
33689
+ return this.dicomDataset.modality;
33690
+ }
33691
+ /** Accession Number (0008,0050). */
33692
+ get accession() {
33693
+ return this.dicomDataset.accession;
33694
+ }
33695
+ /** SOP Class UID (0008,0016). */
33696
+ get sopClassUID() {
33697
+ return this.dicomDataset.sopClassUID;
33698
+ }
33699
+ /** Study Instance UID (0020,000D). */
33700
+ get studyInstanceUID() {
33701
+ return this.dicomDataset.studyInstanceUID;
33702
+ }
33703
+ /** Series Instance UID (0020,000E). */
33704
+ get seriesInstanceUID() {
33705
+ return this.dicomDataset.seriesInstanceUID;
33706
+ }
33707
+ /** SOP Instance UID (0008,0018). */
33708
+ get sopInstanceUID() {
33709
+ return this.dicomDataset.sopInstanceUID;
33710
+ }
33711
+ /** Transfer Syntax UID (0002,0010). */
33712
+ get transferSyntaxUID() {
33713
+ return this.dicomDataset.transferSyntaxUID;
33714
+ }
33715
+ /**
33716
+ * Gets a tag value as a string with optional fallback.
33717
+ *
33718
+ * @param tag - A DICOM tag, e.g. `'(0010,0010)'` or `'00100010'`
33719
+ * @param fallback - Value to return if tag is missing (default: `''`)
33720
+ */
33721
+ getString(tag2, fallback = "") {
33722
+ return this.dicomDataset.getString(tag2, fallback);
33723
+ }
33724
+ /**
33725
+ * Gets a tag value as a number.
33726
+ *
33727
+ * @param tag - A DICOM tag, e.g. `'(0020,0013)'`
33728
+ */
33729
+ getNumber(tag2) {
33730
+ return this.dicomDataset.getNumber(tag2);
33731
+ }
33732
+ /** Checks whether a tag exists in the dataset. */
33733
+ hasTag(tag2) {
33734
+ return this.dicomDataset.hasTag(tag2);
33735
+ }
33736
+ /**
33737
+ * Finds all values matching a wildcard path.
33738
+ *
33739
+ * @param path - A DicomTagPath with optional wildcard indices
33740
+ */
33741
+ findValues(path2) {
33742
+ return this.dicomDataset.findValues(path2);
33743
+ }
33744
+ // -----------------------------------------------------------------------
33745
+ // Write methods (return new instance)
33746
+ // -----------------------------------------------------------------------
33747
+ /**
33748
+ * Sets a tag value, returning a new DicomInstance.
33749
+ *
33750
+ * @param path - The DICOM tag path (e.g. `'(0010,0010)'`)
33751
+ * @param value - The new value
33752
+ */
33753
+ setTag(path2, value) {
33754
+ return new _DicomInstance(this.dicomDataset, this.changeSet.setTag(path2, value), this.filepath, this.meta);
33755
+ }
33756
+ /**
33757
+ * Erases a tag, returning a new DicomInstance.
33758
+ *
33759
+ * @param path - The DICOM tag path to erase
33760
+ */
33761
+ eraseTag(path2) {
33762
+ return new _DicomInstance(this.dicomDataset, this.changeSet.eraseTag(path2), this.filepath, this.meta);
33763
+ }
33764
+ /** Erases all private tags, returning a new DicomInstance. */
33765
+ erasePrivateTags() {
33766
+ return new _DicomInstance(this.dicomDataset, this.changeSet.erasePrivateTags(), this.filepath, this.meta);
33767
+ }
33768
+ /** Sets Patient's Name (0010,0010). */
33769
+ setPatientName(value) {
33770
+ return this.setTag("(0010,0010)", value);
33771
+ }
33772
+ /** Sets Patient ID (0010,0020). */
33773
+ setPatientID(value) {
33774
+ return this.setTag("(0010,0020)", value);
33775
+ }
33776
+ /** Sets Study Date (0008,0020). */
33777
+ setStudyDate(value) {
33778
+ return this.setTag("(0008,0020)", value);
33779
+ }
33780
+ /** Sets Modality (0008,0060). */
33781
+ setModality(value) {
33782
+ return this.setTag("(0008,0060)", value);
33783
+ }
33784
+ /** Sets Accession Number (0008,0050). */
33785
+ setAccessionNumber(value) {
33786
+ return this.setTag("(0008,0050)", value);
33787
+ }
33788
+ /** Sets Study Description (0008,1030). */
33789
+ setStudyDescription(value) {
33790
+ return this.setTag("(0008,1030)", value);
33791
+ }
33792
+ /** Sets Series Description (0008,103E). */
33793
+ setSeriesDescription(value) {
33794
+ return this.setTag("(0008,103E)", value);
33795
+ }
33796
+ /** Sets Institution Name (0008,0080). */
33797
+ setInstitutionName(value) {
33798
+ return this.setTag("(0008,0080)", value);
33799
+ }
33800
+ /**
33801
+ * Transforms a tag value using a function.
33802
+ *
33803
+ * The function receives the current string value (or undefined if tag is missing)
33804
+ * and returns the new value. Returns a new DicomInstance with the modification.
33805
+ *
33806
+ * @param path - The DICOM tag path
33807
+ * @param fn - Transform function receiving the current value
33808
+ */
33809
+ transformTag(path2, fn) {
33810
+ const current = this.dicomDataset.getString(path2);
33811
+ const newValue = fn(current.length > 0 ? current : void 0);
33812
+ return this.setTag(path2, newValue);
33813
+ }
33814
+ /**
33815
+ * Sets multiple tags at once, returning a new DicomInstance.
33816
+ *
33817
+ * @param entries - A record of tag path → value pairs
33818
+ */
33819
+ setBatch(entries) {
33820
+ return new _DicomInstance(this.dicomDataset, this.changeSet.setBatch(entries), this.filepath, this.meta);
33821
+ }
33822
+ /**
33823
+ * Returns a new DicomInstance with the given changes merged into pending changes.
33824
+ *
33825
+ * @param changes - A ChangeSet to merge with existing pending changes
33826
+ * @returns A new DicomInstance with accumulated changes
33827
+ */
33828
+ withChanges(changes) {
33829
+ return new _DicomInstance(this.dicomDataset, this.changeSet.merge(changes), this.filepath, this.meta);
33830
+ }
33831
+ /**
33832
+ * Returns a new DicomInstance pointing to a different file path.
33833
+ *
33834
+ * Preserves the dataset, pending changes, and metadata.
33835
+ *
33836
+ * @param newPath - The new filesystem path (validated via createDicomFilePath)
33837
+ * @returns A new DicomInstance with the updated path
33838
+ * @throws If the path is invalid
33839
+ */
33840
+ withFilePath(newPath) {
33841
+ const result = createDicomFilePath(newPath);
33842
+ if (!result.ok) throw result.error;
33843
+ return new _DicomInstance(this.dicomDataset, this.changeSet, result.value, this.meta);
33844
+ }
33845
+ // -----------------------------------------------------------------------
33846
+ // File I/O
33847
+ // -----------------------------------------------------------------------
33848
+ /**
33849
+ * Applies pending changes to the file in-place.
33850
+ *
33851
+ * Requires that the instance has an associated file path.
33852
+ *
33853
+ * @param options - Timeout and abort options
33854
+ */
33855
+ async applyChanges(options) {
33856
+ if (this.filepath === void 0) return err(new Error("No file path associated with this instance"));
33857
+ if (this.changeSet.isEmpty) return ok(void 0);
33858
+ return applyModifications(this.filepath, this.changeSet, options ?? {});
33859
+ }
33860
+ /**
33861
+ * Copies the file to a new path and applies pending changes to the copy.
33862
+ *
33863
+ * Returns a new DicomInstance pointing to the output path.
33864
+ *
33865
+ * @param outputPath - Destination filesystem path
33866
+ * @param options - Timeout and abort options
33867
+ */
33868
+ async writeAs(outputPath, options) {
33869
+ if (this.filepath === void 0) return err(new Error("No file path associated with this instance"));
33870
+ const outPathResult = createDicomFilePath(outputPath);
33871
+ if (!outPathResult.ok) return err(outPathResult.error);
33872
+ const copyResult = await copyFileSafe(this.filepath, outputPath);
33873
+ if (!copyResult.ok) return err(copyResult.error);
33874
+ if (!this.changeSet.isEmpty) {
33875
+ const applyResult = await applyModifications(outPathResult.value, this.changeSet, options ?? {});
33876
+ if (!applyResult.ok) {
33877
+ await unlinkFile(outputPath);
33878
+ return err(applyResult.error);
33879
+ }
33880
+ }
33881
+ return ok(new _DicomInstance(this.dicomDataset, ChangeSet.empty(), outPathResult.value, this.meta));
33882
+ }
33883
+ /**
33884
+ * Gets the file size in bytes.
33885
+ *
33886
+ * @returns A Result containing the size or an error
33887
+ */
33888
+ async fileSize() {
33889
+ if (this.filepath === void 0) return err(new Error("No file path associated with this instance"));
33890
+ return statFileSize(this.filepath);
33891
+ }
33892
+ /**
33893
+ * Deletes the associated file from the filesystem.
33894
+ *
33895
+ * @returns A Result indicating success or failure
33896
+ */
33897
+ async unlink() {
33898
+ if (this.filepath === void 0) return err(new Error("No file path associated with this instance"));
33899
+ return unlinkFile(this.filepath);
33900
+ }
33901
+ // -----------------------------------------------------------------------
33902
+ // Metadata (non-DICOM app context)
33903
+ // -----------------------------------------------------------------------
33904
+ /**
33905
+ * Returns a new DicomInstance with application metadata attached.
33906
+ *
33907
+ * Metadata is not stored in the DICOM file — it's for application context
33908
+ * (e.g. tracking source association, processing status, etc.).
33909
+ *
33910
+ * @param key - Metadata key
33911
+ * @param value - Metadata value
33912
+ */
33913
+ withMetadata(key, value) {
33914
+ const newMeta = new Map(this.meta);
33915
+ newMeta.set(key, value);
33916
+ return new _DicomInstance(this.dicomDataset, this.changeSet, this.filepath, newMeta);
33917
+ }
33918
+ /**
33919
+ * Gets application metadata by key.
33920
+ *
33921
+ * @param key - Metadata key
33922
+ * @returns The metadata value or undefined
33923
+ */
33924
+ getMetadata(key) {
33925
+ return this.meta.get(key);
33926
+ }
33927
+ };
33584
33928
 
33585
33929
  // src/events/dcmrecv.ts
33586
33930
  var DcmrecvEvent = {
@@ -33593,7 +33937,11 @@ var DcmrecvEvent = {
33593
33937
  ASSOCIATION_ABORTED: "ASSOCIATION_ABORTED",
33594
33938
  ECHO_REQUEST: "ECHO_REQUEST",
33595
33939
  CANNOT_START_LISTENER: "CANNOT_START_LISTENER",
33596
- REFUSING_ASSOCIATION: "REFUSING_ASSOCIATION"
33940
+ REFUSING_ASSOCIATION: "REFUSING_ASSOCIATION",
33941
+ /** Synthetic: STORED_FILE enriched with association context. */
33942
+ FILE_RECEIVED: "FILE_RECEIVED",
33943
+ /** Synthetic: emitted on association release/abort with summary. */
33944
+ ASSOCIATION_COMPLETE: "ASSOCIATION_COMPLETE"
33597
33945
  };
33598
33946
  var DCMRECV_PATTERNS = [
33599
33947
  {
@@ -33603,9 +33951,9 @@ var DCMRECV_PATTERNS = [
33603
33951
  },
33604
33952
  {
33605
33953
  event: DcmrecvEvent.ASSOCIATION_RECEIVED,
33606
- pattern: /Association Received\s{1,100}([^:]+):\s*"([^"]+)"\s*->\s*"([^"]+)"/,
33954
+ pattern: /Association Received\s{1,100}(\S+):\s+(\S+)\s+->\s+(\S+)/,
33607
33955
  processor: (match) => ({
33608
- address: match[1] ?? "",
33956
+ source: match[1] ?? "",
33609
33957
  callingAE: match[2] ?? "",
33610
33958
  calledAE: match[3] ?? ""
33611
33959
  })
@@ -33633,7 +33981,7 @@ var DCMRECV_PATTERNS = [
33633
33981
  },
33634
33982
  {
33635
33983
  event: DcmrecvEvent.ASSOCIATION_RELEASE,
33636
- pattern: /Received Association Release/i,
33984
+ pattern: /Association Release/i,
33637
33985
  processor: () => void 0
33638
33986
  },
33639
33987
  {
@@ -33669,6 +34017,11 @@ var StorescpEvent = {
33669
34017
  STORING_FILE: "STORING_FILE",
33670
34018
  SUBDIRECTORY_CREATED: "SUBDIRECTORY_CREATED"
33671
34019
  };
34020
+ var STORESCP_ASSOCIATION_RECEIVED = {
34021
+ event: StorescpEvent.ASSOCIATION_RECEIVED,
34022
+ pattern: /Association Received/i,
34023
+ processor: () => ({ source: "", callingAE: "", calledAE: "" })
34024
+ };
33672
34025
  var STORESCP_ADDITIONAL_PATTERNS = [
33673
34026
  {
33674
34027
  event: StorescpEvent.STORING_FILE,
@@ -33685,7 +34038,11 @@ var STORESCP_ADDITIONAL_PATTERNS = [
33685
34038
  })
33686
34039
  }
33687
34040
  ];
33688
- var STORESCP_PATTERNS = [...DCMRECV_PATTERNS, ...STORESCP_ADDITIONAL_PATTERNS];
34041
+ var STORESCP_PATTERNS = [
34042
+ ...DCMRECV_PATTERNS.filter((p) => p.event !== DcmrecvEvent.ASSOCIATION_RECEIVED),
34043
+ STORESCP_ASSOCIATION_RECEIVED,
34044
+ ...STORESCP_ADDITIONAL_PATTERNS
34045
+ ];
33689
34046
  var STORESCP_FATAL_EVENTS = /* @__PURE__ */ new Set([...DCMRECV_FATAL_EVENTS]);
33690
34047
 
33691
34048
  // src/events/dcmprscp.ts
@@ -33992,6 +34349,97 @@ var WLMSCPFS_PATTERNS = [
33992
34349
  }
33993
34350
  ];
33994
34351
  var WLMSCPFS_FATAL_EVENTS = /* @__PURE__ */ new Set([WlmscpfsEvent.CANNOT_START_LISTENER]);
34352
+
34353
+ // src/servers/AssociationTracker.ts
34354
+ var AssociationTracker = class {
34355
+ constructor() {
34356
+ __publicField(this, "association");
34357
+ __publicField(this, "counter", 0);
34358
+ }
34359
+ /**
34360
+ * Begins a new association, transitioning from IDLE to ACTIVE.
34361
+ *
34362
+ * If an association is already active, it is silently ended (abort)
34363
+ * and the new one begins.
34364
+ *
34365
+ * @param data - Association metadata
34366
+ * @returns The unique association ID
34367
+ */
34368
+ beginAssociation(data) {
34369
+ this.counter++;
34370
+ const associationId = `assoc-${String(this.counter)}`;
34371
+ this.association = {
34372
+ associationId,
34373
+ callingAE: data.callingAE,
34374
+ calledAE: data.calledAE,
34375
+ source: data.source,
34376
+ startTime: Date.now(),
34377
+ files: []
34378
+ };
34379
+ return associationId;
34380
+ }
34381
+ /**
34382
+ * Tracks a file received during the current association.
34383
+ *
34384
+ * If no association is active, returns a TrackedFile with empty context.
34385
+ *
34386
+ * @param filePath - Path to the received file
34387
+ * @returns A TrackedFile enriched with association context
34388
+ */
34389
+ trackFile(filePath) {
34390
+ if (this.association === void 0) {
34391
+ return {
34392
+ filePath,
34393
+ associationId: "",
34394
+ callingAE: "",
34395
+ calledAE: "",
34396
+ source: ""
34397
+ };
34398
+ }
34399
+ this.association.files.push(filePath);
34400
+ return {
34401
+ filePath,
34402
+ associationId: this.association.associationId,
34403
+ callingAE: this.association.callingAE,
34404
+ calledAE: this.association.calledAE,
34405
+ source: this.association.source
34406
+ };
34407
+ }
34408
+ /**
34409
+ * Ends the current association, transitioning from ACTIVE to IDLE.
34410
+ *
34411
+ * @param reason - Why the association ended
34412
+ * @returns An AssociationSummary, or undefined if no association was active
34413
+ */
34414
+ endAssociation(reason) {
34415
+ if (this.association === void 0) return void 0;
34416
+ const summary = {
34417
+ associationId: this.association.associationId,
34418
+ callingAE: this.association.callingAE,
34419
+ calledAE: this.association.calledAE,
34420
+ source: this.association.source,
34421
+ files: [...this.association.files],
34422
+ durationMs: Date.now() - this.association.startTime,
34423
+ endReason: reason
34424
+ };
34425
+ this.association = void 0;
34426
+ return summary;
34427
+ }
34428
+ /** The currently active association context, or undefined. */
34429
+ get current() {
34430
+ return this.association;
34431
+ }
34432
+ /** Whether an association is currently active. */
34433
+ get isActive() {
34434
+ return this.association !== void 0;
34435
+ }
34436
+ /** Resets the tracker to IDLE, discarding any active association. */
34437
+ reset() {
34438
+ this.association = void 0;
34439
+ }
34440
+ };
34441
+
34442
+ // src/servers/Dcmrecv.ts
33995
34443
  var SubdirectoryMode = {
33996
34444
  NONE: "none",
33997
34445
  SERIES_DATE: "series-date"
@@ -34078,10 +34526,13 @@ var Dcmrecv = class _Dcmrecv extends DcmtkProcess {
34078
34526
  constructor(config, parser2, signal) {
34079
34527
  super(config);
34080
34528
  __publicField(this, "parser");
34529
+ __publicField(this, "tracker");
34081
34530
  __publicField(this, "abortSignal");
34082
34531
  __publicField(this, "abortHandler");
34083
34532
  this.parser = parser2;
34533
+ this.tracker = new AssociationTracker();
34084
34534
  this.wireParser();
34535
+ this.wireTracker();
34085
34536
  if (signal !== void 0) {
34086
34537
  this.wireAbortSignal(signal);
34087
34538
  }
@@ -34122,6 +34573,24 @@ var Dcmrecv = class _Dcmrecv extends DcmtkProcess {
34122
34573
  onStoredFile(listener) {
34123
34574
  return this.onEvent("STORED_FILE", listener);
34124
34575
  }
34576
+ /**
34577
+ * Registers a listener for received files enriched with association context.
34578
+ *
34579
+ * @param listener - Callback receiving tracked file data
34580
+ * @returns this for chaining
34581
+ */
34582
+ onFileReceived(listener) {
34583
+ return this.onEvent("FILE_RECEIVED", listener);
34584
+ }
34585
+ /**
34586
+ * Registers a listener for completed associations.
34587
+ *
34588
+ * @param listener - Callback receiving association summary
34589
+ * @returns this for chaining
34590
+ */
34591
+ onAssociationComplete(listener) {
34592
+ return this.onEvent("ASSOCIATION_COMPLETE", listener);
34593
+ }
34125
34594
  /**
34126
34595
  * Creates a new Dcmrecv server instance.
34127
34596
  *
@@ -34131,7 +34600,7 @@ var Dcmrecv = class _Dcmrecv extends DcmtkProcess {
34131
34600
  static create(options) {
34132
34601
  const validation = DcmrecvOptionsSchema.safeParse(options);
34133
34602
  if (!validation.success) {
34134
- return err(new Error(`dcmrecv: invalid options: ${validation.error.message}`));
34603
+ return err(createValidationError("dcmrecv", validation.error));
34135
34604
  }
34136
34605
  const binaryResult = resolveBinary("dcmrecv");
34137
34606
  if (!binaryResult.ok) {
@@ -34164,7 +34633,29 @@ var Dcmrecv = class _Dcmrecv extends DcmtkProcess {
34164
34633
  this.emit("error", { error: new Error(`Fatal: ${event}`), fatal: true });
34165
34634
  void this.stop();
34166
34635
  }
34167
- this.emit(event, ...[data]);
34636
+ this.emit(event, data);
34637
+ });
34638
+ }
34639
+ /** Wires the AssociationTracker to server events. */
34640
+ wireTracker() {
34641
+ this.onEvent("ASSOCIATION_RECEIVED", (data) => {
34642
+ this.tracker.beginAssociation(data);
34643
+ });
34644
+ this.onEvent("STORED_FILE", (data) => {
34645
+ const tracked = this.tracker.trackFile(data.filePath);
34646
+ this.emit(DcmrecvEvent.FILE_RECEIVED, tracked);
34647
+ });
34648
+ this.onEvent("ASSOCIATION_RELEASE", () => {
34649
+ const summary = this.tracker.endAssociation("release");
34650
+ if (summary !== void 0) {
34651
+ this.emit(DcmrecvEvent.ASSOCIATION_COMPLETE, summary);
34652
+ }
34653
+ });
34654
+ this.onEvent("ASSOCIATION_ABORTED", () => {
34655
+ const summary = this.tracker.endAssociation("abort");
34656
+ if (summary !== void 0) {
34657
+ this.emit(DcmrecvEvent.ASSOCIATION_COMPLETE, summary);
34658
+ }
34168
34659
  });
34169
34660
  }
34170
34661
  /** Wires an AbortSignal to stop the server. */
@@ -34312,21 +34803,17 @@ var StoreSCP = class _StoreSCP extends DcmtkProcess {
34312
34803
  constructor(config, parser2, signal) {
34313
34804
  super(config);
34314
34805
  __publicField(this, "parser");
34806
+ __publicField(this, "tracker");
34315
34807
  __publicField(this, "abortSignal");
34316
34808
  __publicField(this, "abortHandler");
34317
34809
  this.parser = parser2;
34810
+ this.tracker = new AssociationTracker();
34318
34811
  this.wireParser();
34812
+ this.wireTracker();
34319
34813
  if (signal !== void 0) {
34320
34814
  this.wireAbortSignal(signal);
34321
34815
  }
34322
34816
  }
34323
- /**
34324
- * Registers a typed listener for a storescp-specific event.
34325
- *
34326
- * @param event - The event name from StoreSCPEventMap
34327
- * @param listener - Callback receiving typed event data
34328
- * @returns this for chaining
34329
- */
34330
34817
  /** Disposes the server and its parser, preventing listener leaks. */
34331
34818
  [Symbol.dispose]() {
34332
34819
  if (this.abortSignal !== void 0 && this.abortHandler !== void 0) {
@@ -34335,6 +34822,13 @@ var StoreSCP = class _StoreSCP extends DcmtkProcess {
34335
34822
  this.parser[Symbol.dispose]();
34336
34823
  super[Symbol.dispose]();
34337
34824
  }
34825
+ /**
34826
+ * Registers a typed listener for a storescp-specific event.
34827
+ *
34828
+ * @param event - The event name from StoreSCPEventMap
34829
+ * @param listener - Callback receiving typed event data
34830
+ * @returns this for chaining
34831
+ */
34338
34832
  onEvent(event, listener) {
34339
34833
  return this.on(event, listener);
34340
34834
  }
@@ -34356,6 +34850,24 @@ var StoreSCP = class _StoreSCP extends DcmtkProcess {
34356
34850
  onStoringFile(listener) {
34357
34851
  return this.onEvent("STORING_FILE", listener);
34358
34852
  }
34853
+ /**
34854
+ * Registers a listener for received files enriched with association context.
34855
+ *
34856
+ * @param listener - Callback receiving tracked file data
34857
+ * @returns this for chaining
34858
+ */
34859
+ onFileReceived(listener) {
34860
+ return this.onEvent("FILE_RECEIVED", listener);
34861
+ }
34862
+ /**
34863
+ * Registers a listener for completed associations.
34864
+ *
34865
+ * @param listener - Callback receiving association summary
34866
+ * @returns this for chaining
34867
+ */
34868
+ onAssociationComplete(listener) {
34869
+ return this.onEvent("ASSOCIATION_COMPLETE", listener);
34870
+ }
34359
34871
  /**
34360
34872
  * Creates a new StoreSCP server instance.
34361
34873
  *
@@ -34365,7 +34877,7 @@ var StoreSCP = class _StoreSCP extends DcmtkProcess {
34365
34877
  static create(options) {
34366
34878
  const validation = StoreSCPOptionsSchema.safeParse(options);
34367
34879
  if (!validation.success) {
34368
- return err(new Error(`storescp: invalid options: ${validation.error.message}`));
34880
+ return err(createValidationError("storescp", validation.error));
34369
34881
  }
34370
34882
  const binaryResult = resolveBinary("storescp");
34371
34883
  if (!binaryResult.ok) {
@@ -34398,7 +34910,33 @@ var StoreSCP = class _StoreSCP extends DcmtkProcess {
34398
34910
  this.emit("error", { error: new Error(`Fatal: ${event}`), fatal: true });
34399
34911
  void this.stop();
34400
34912
  }
34401
- this.emit(event, ...[data]);
34913
+ this.emit(event, data);
34914
+ });
34915
+ }
34916
+ /** Wires the AssociationTracker to server events. */
34917
+ wireTracker() {
34918
+ this.onEvent("ASSOCIATION_RECEIVED", (data) => {
34919
+ this.tracker.beginAssociation(data);
34920
+ });
34921
+ this.onEvent("STORING_FILE", (data) => {
34922
+ const tracked = this.tracker.trackFile(data.filePath);
34923
+ this.emit(DcmrecvEvent.FILE_RECEIVED, tracked);
34924
+ });
34925
+ this.onEvent("STORED_FILE", (data) => {
34926
+ const tracked = this.tracker.trackFile(data.filePath);
34927
+ this.emit(DcmrecvEvent.FILE_RECEIVED, tracked);
34928
+ });
34929
+ this.onEvent("ASSOCIATION_RELEASE", () => {
34930
+ const summary = this.tracker.endAssociation("release");
34931
+ if (summary !== void 0) {
34932
+ this.emit(DcmrecvEvent.ASSOCIATION_COMPLETE, summary);
34933
+ }
34934
+ });
34935
+ this.onEvent("ASSOCIATION_ABORTED", () => {
34936
+ const summary = this.tracker.endAssociation("abort");
34937
+ if (summary !== void 0) {
34938
+ this.emit(DcmrecvEvent.ASSOCIATION_COMPLETE, summary);
34939
+ }
34402
34940
  });
34403
34941
  }
34404
34942
  /** Wires an AbortSignal to stop the server. */
@@ -34497,7 +35035,7 @@ var DcmprsCP = class _DcmprsCP extends DcmtkProcess {
34497
35035
  static create(options) {
34498
35036
  const validation = DcmprsCPOptionsSchema.safeParse(options);
34499
35037
  if (!validation.success) {
34500
- return err(new Error(`dcmprscp: invalid options: ${validation.error.message}`));
35038
+ return err(createValidationError("dcmprscp", validation.error));
34501
35039
  }
34502
35040
  const binaryResult = resolveBinary("dcmprscp");
34503
35041
  if (!binaryResult.ok) {
@@ -34530,7 +35068,7 @@ var DcmprsCP = class _DcmprsCP extends DcmtkProcess {
34530
35068
  this.emit("error", { error: new Error(`Fatal: ${event}`), fatal: true });
34531
35069
  void this.stop();
34532
35070
  }
34533
- this.emit(event, ...[data]);
35071
+ this.emit(event, data);
34534
35072
  });
34535
35073
  }
34536
35074
  /** Wires an AbortSignal to stop the server. */
@@ -34626,7 +35164,7 @@ var Dcmpsrcv = class _Dcmpsrcv extends DcmtkProcess {
34626
35164
  static create(options) {
34627
35165
  const validation = DcmpsrcvOptionsSchema.safeParse(options);
34628
35166
  if (!validation.success) {
34629
- return err(new Error(`dcmpsrcv: invalid options: ${validation.error.message}`));
35167
+ return err(createValidationError("dcmpsrcv", validation.error));
34630
35168
  }
34631
35169
  const binaryResult = resolveBinary("dcmpsrcv");
34632
35170
  if (!binaryResult.ok) {
@@ -34659,7 +35197,7 @@ var Dcmpsrcv = class _Dcmpsrcv extends DcmtkProcess {
34659
35197
  this.emit("error", { error: new Error(`Fatal: ${event}`), fatal: true });
34660
35198
  void this.stop();
34661
35199
  }
34662
- this.emit(event, ...[data]);
35200
+ this.emit(event, data);
34663
35201
  });
34664
35202
  }
34665
35203
  /** Wires an AbortSignal to stop the server. */
@@ -34737,13 +35275,6 @@ var DcmQRSCP = class _DcmQRSCP extends DcmtkProcess {
34737
35275
  this.wireAbortSignal(signal);
34738
35276
  }
34739
35277
  }
34740
- /**
34741
- * Registers a typed listener for a dcmqrscp-specific event.
34742
- *
34743
- * @param event - The event name from DcmQRSCPEventMap
34744
- * @param listener - Callback receiving typed event data
34745
- * @returns this for chaining
34746
- */
34747
35278
  /** Disposes the server and its parser, preventing listener leaks. */
34748
35279
  [Symbol.dispose]() {
34749
35280
  if (this.abortSignal !== void 0 && this.abortHandler !== void 0) {
@@ -34752,6 +35283,13 @@ var DcmQRSCP = class _DcmQRSCP extends DcmtkProcess {
34752
35283
  this.parser[Symbol.dispose]();
34753
35284
  super[Symbol.dispose]();
34754
35285
  }
35286
+ /**
35287
+ * Registers a typed listener for a dcmqrscp-specific event.
35288
+ *
35289
+ * @param event - The event name from DcmQRSCPEventMap
35290
+ * @param listener - Callback receiving typed event data
35291
+ * @returns this for chaining
35292
+ */
34755
35293
  onEvent(event, listener) {
34756
35294
  return this.on(event, listener);
34757
35295
  }
@@ -34782,7 +35320,7 @@ var DcmQRSCP = class _DcmQRSCP extends DcmtkProcess {
34782
35320
  static create(options) {
34783
35321
  const validation = DcmQRSCPOptionsSchema.safeParse(options);
34784
35322
  if (!validation.success) {
34785
- return err(new Error(`dcmqrscp: invalid options: ${validation.error.message}`));
35323
+ return err(createValidationError("dcmqrscp", validation.error));
34786
35324
  }
34787
35325
  const binaryResult = resolveBinary("dcmqrscp");
34788
35326
  if (!binaryResult.ok) {
@@ -34814,7 +35352,7 @@ var DcmQRSCP = class _DcmQRSCP extends DcmtkProcess {
34814
35352
  this.emit("error", { error: new Error(`Fatal: ${event}`), fatal: true });
34815
35353
  void this.stop();
34816
35354
  }
34817
- this.emit(event, ...[data]);
35355
+ this.emit(event, data);
34818
35356
  });
34819
35357
  }
34820
35358
  /** Wires an AbortSignal to stop the server. */
@@ -34884,13 +35422,6 @@ var Wlmscpfs = class _Wlmscpfs extends DcmtkProcess {
34884
35422
  this.wireAbortSignal(signal);
34885
35423
  }
34886
35424
  }
34887
- /**
34888
- * Registers a typed listener for a wlmscpfs-specific event.
34889
- *
34890
- * @param event - The event name from WlmscpfsEventMap
34891
- * @param listener - Callback receiving typed event data
34892
- * @returns this for chaining
34893
- */
34894
35425
  /** Disposes the server and its parser, preventing listener leaks. */
34895
35426
  [Symbol.dispose]() {
34896
35427
  if (this.abortSignal !== void 0 && this.abortHandler !== void 0) {
@@ -34899,6 +35430,13 @@ var Wlmscpfs = class _Wlmscpfs extends DcmtkProcess {
34899
35430
  this.parser[Symbol.dispose]();
34900
35431
  super[Symbol.dispose]();
34901
35432
  }
35433
+ /**
35434
+ * Registers a typed listener for a wlmscpfs-specific event.
35435
+ *
35436
+ * @param event - The event name from WlmscpfsEventMap
35437
+ * @param listener - Callback receiving typed event data
35438
+ * @returns this for chaining
35439
+ */
34902
35440
  onEvent(event, listener) {
34903
35441
  return this.on(event, listener);
34904
35442
  }
@@ -34929,7 +35467,7 @@ var Wlmscpfs = class _Wlmscpfs extends DcmtkProcess {
34929
35467
  static create(options) {
34930
35468
  const validation = WlmscpfsOptionsSchema.safeParse(options);
34931
35469
  if (!validation.success) {
34932
- return err(new Error(`wlmscpfs: invalid options: ${validation.error.message}`));
35470
+ return err(createValidationError("wlmscpfs", validation.error));
34933
35471
  }
34934
35472
  const binaryResult = resolveBinary("wlmscpfs");
34935
35473
  if (!binaryResult.ok) {
@@ -34962,7 +35500,7 @@ var Wlmscpfs = class _Wlmscpfs extends DcmtkProcess {
34962
35500
  this.emit("error", { error: new Error(`Fatal: ${event}`), fatal: true });
34963
35501
  void this.stop();
34964
35502
  }
34965
- this.emit(event, ...[data]);
35503
+ this.emit(event, data);
34966
35504
  });
34967
35505
  }
34968
35506
  /** Wires an AbortSignal to stop the server. */
@@ -34978,6 +35516,506 @@ var Wlmscpfs = class _Wlmscpfs extends DcmtkProcess {
34978
35516
  signal.addEventListener("abort", this.abortHandler, { once: true });
34979
35517
  }
34980
35518
  };
35519
+ var DEFAULT_MIN_POOL_SIZE = 2;
35520
+ var DEFAULT_MAX_POOL_SIZE = 10;
35521
+ var DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
35522
+ var CONNECTION_RETRY_INTERVAL_MS = 500;
35523
+ var MAX_CONNECTION_RETRIES = 200;
35524
+ var DicomReceiverOptionsSchema = zod.z.object({
35525
+ port: zod.z.number().int().min(1).max(65535),
35526
+ storageDir: zod.z.string().min(1).refine(isSafePath, { message: "path traversal detected in storageDir" }),
35527
+ aeTitle: zod.z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
35528
+ minPoolSize: zod.z.number().int().min(1).max(100).optional(),
35529
+ maxPoolSize: zod.z.number().int().min(1).max(100).optional(),
35530
+ connectionTimeoutMs: zod.z.number().int().positive().optional(),
35531
+ configFile: zod.z.string().min(1).refine(isSafePath, { message: "path traversal detected in configFile" }).optional(),
35532
+ configProfile: zod.z.string().min(1).optional(),
35533
+ signal: zod.z.instanceof(AbortSignal).optional()
35534
+ }).strict().refine((data) => (data.minPoolSize ?? DEFAULT_MIN_POOL_SIZE) <= (data.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE), {
35535
+ message: "minPoolSize must be <= maxPoolSize"
35536
+ });
35537
+ function allocatePort() {
35538
+ return new Promise((resolve) => {
35539
+ const server = net__namespace.createServer();
35540
+ server.listen(0, "127.0.0.1", () => {
35541
+ const addr = server.address();
35542
+ if (addr === null || typeof addr === "string") {
35543
+ server.close(() => resolve(err(new Error("Failed to allocate port"))));
35544
+ return;
35545
+ }
35546
+ const port = addr.port;
35547
+ server.close(() => resolve(ok(port)));
35548
+ });
35549
+ server.on("error", (e) => {
35550
+ resolve(err(new Error(`Port allocation failed: ${e.message}`)));
35551
+ });
35552
+ });
35553
+ }
35554
+ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
35555
+ constructor(options) {
35556
+ super();
35557
+ __publicField(this, "options");
35558
+ __publicField(this, "minPoolSize");
35559
+ __publicField(this, "maxPoolSize");
35560
+ __publicField(this, "connectionTimeoutMs");
35561
+ __publicField(this, "workers", /* @__PURE__ */ new Map());
35562
+ __publicField(this, "tcpServer");
35563
+ __publicField(this, "associationCounter", 0);
35564
+ __publicField(this, "started", false);
35565
+ __publicField(this, "stopping", false);
35566
+ __publicField(this, "abortHandler");
35567
+ this.setMaxListeners(20);
35568
+ this.on("error", () => {
35569
+ });
35570
+ this.options = options;
35571
+ this.minPoolSize = options.minPoolSize ?? DEFAULT_MIN_POOL_SIZE;
35572
+ this.maxPoolSize = options.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE;
35573
+ this.connectionTimeoutMs = options.connectionTimeoutMs ?? DEFAULT_CONNECTION_TIMEOUT_MS;
35574
+ }
35575
+ // -----------------------------------------------------------------------
35576
+ // Public API
35577
+ // -----------------------------------------------------------------------
35578
+ /**
35579
+ * Creates a new DicomReceiver instance.
35580
+ *
35581
+ * @param options - Configuration options
35582
+ * @returns A Result containing the instance or a validation error
35583
+ */
35584
+ static create(options) {
35585
+ const validation = DicomReceiverOptionsSchema.safeParse(options);
35586
+ if (!validation.success) {
35587
+ return err(createValidationError("DicomReceiver", validation.error));
35588
+ }
35589
+ return ok(new _DicomReceiver(options));
35590
+ }
35591
+ /**
35592
+ * Starts the TCP proxy and spawns the initial worker pool.
35593
+ *
35594
+ * @returns A Result indicating success or failure
35595
+ */
35596
+ async start() {
35597
+ if (this.started) {
35598
+ return err(new Error("DicomReceiver: already started"));
35599
+ }
35600
+ this.started = true;
35601
+ const storageDirResult = await ensureDirectory(this.options.storageDir);
35602
+ if (!storageDirResult.ok) return storageDirResult;
35603
+ const spawnResults = await this.spawnWorkers(this.minPoolSize);
35604
+ if (!spawnResults.ok) return spawnResults;
35605
+ const listenResult = await this.startTcpProxy();
35606
+ if (!listenResult.ok) return listenResult;
35607
+ if (this.options.signal !== void 0) {
35608
+ this.wireAbortSignal(this.options.signal);
35609
+ }
35610
+ return ok(void 0);
35611
+ }
35612
+ /**
35613
+ * Stops the TCP proxy and all workers.
35614
+ */
35615
+ async stop() {
35616
+ if (!this.started || this.stopping) {
35617
+ return;
35618
+ }
35619
+ this.stopping = true;
35620
+ if (this.options.signal !== void 0 && this.abortHandler !== void 0) {
35621
+ this.options.signal.removeEventListener("abort", this.abortHandler);
35622
+ }
35623
+ await this.closeTcpProxy();
35624
+ const stopPromises = [];
35625
+ for (const worker of this.workers.values()) {
35626
+ stopPromises.push(this.stopWorker(worker));
35627
+ }
35628
+ await Promise.all(stopPromises);
35629
+ this.workers.clear();
35630
+ this.started = false;
35631
+ this.stopping = false;
35632
+ }
35633
+ /**
35634
+ * Registers a typed listener for a DicomReceiver-specific event.
35635
+ *
35636
+ * @param event - The event name from DicomReceiverEventMap
35637
+ * @param listener - Callback receiving typed event data
35638
+ * @returns this for chaining
35639
+ */
35640
+ onEvent(event, listener) {
35641
+ return this.on(event, listener);
35642
+ }
35643
+ /**
35644
+ * Registers a listener for received files.
35645
+ *
35646
+ * @param listener - Callback receiving file data
35647
+ * @returns this for chaining
35648
+ */
35649
+ onFileReceived(listener) {
35650
+ return this.on("FILE_RECEIVED", listener);
35651
+ }
35652
+ /**
35653
+ * Registers a listener for completed associations.
35654
+ *
35655
+ * @param listener - Callback receiving association data
35656
+ * @returns this for chaining
35657
+ */
35658
+ onAssociationComplete(listener) {
35659
+ return this.on("ASSOCIATION_COMPLETE", listener);
35660
+ }
35661
+ /** Current pool status. */
35662
+ get poolStatus() {
35663
+ let idle = 0;
35664
+ let busy = 0;
35665
+ for (const w of this.workers.values()) {
35666
+ if (w.state === "idle") idle++;
35667
+ else busy++;
35668
+ }
35669
+ return { idle, busy, total: this.workers.size };
35670
+ }
35671
+ // -----------------------------------------------------------------------
35672
+ // TCP proxy
35673
+ // -----------------------------------------------------------------------
35674
+ /** Starts the TCP proxy on the configured port. */
35675
+ startTcpProxy() {
35676
+ return new Promise((resolve) => {
35677
+ this.tcpServer = net__namespace.createServer((socket) => {
35678
+ void this.handleConnection(socket);
35679
+ });
35680
+ this.tcpServer.on("error", (e) => {
35681
+ if (!this.started) {
35682
+ resolve(err(new Error(`DicomReceiver: TCP proxy failed: ${e.message}`)));
35683
+ } else {
35684
+ this.emit("error", { error: e instanceof Error ? e : new Error(String(e)) });
35685
+ }
35686
+ });
35687
+ this.tcpServer.listen(this.options.port, () => {
35688
+ resolve(ok(void 0));
35689
+ });
35690
+ });
35691
+ }
35692
+ /** Closes the TCP proxy server. */
35693
+ closeTcpProxy() {
35694
+ return new Promise((resolve) => {
35695
+ if (this.tcpServer === void 0) {
35696
+ resolve();
35697
+ return;
35698
+ }
35699
+ this.tcpServer.close(() => resolve());
35700
+ });
35701
+ }
35702
+ // -----------------------------------------------------------------------
35703
+ // Connection routing
35704
+ // -----------------------------------------------------------------------
35705
+ /** Routes an incoming connection to an idle worker. */
35706
+ async handleConnection(remoteSocket) {
35707
+ remoteSocket.pause();
35708
+ const worker = await this.findIdleWorker();
35709
+ if (worker === void 0) {
35710
+ remoteSocket.destroy(new Error("DicomReceiver: no idle worker available"));
35711
+ this.emit("error", { error: new Error("DicomReceiver: connection rejected \u2014 pool exhausted") });
35712
+ return;
35713
+ }
35714
+ this.associationCounter++;
35715
+ const associationId = `assoc-${String(this.associationCounter)}`;
35716
+ const associationDir = path__namespace.join(this.options.storageDir, associationId);
35717
+ const mkdirResult = await ensureDirectory(associationDir);
35718
+ if (!mkdirResult.ok) {
35719
+ remoteSocket.destroy();
35720
+ this.emit("error", { error: mkdirResult.error });
35721
+ return;
35722
+ }
35723
+ worker.state = "busy";
35724
+ worker.associationId = associationId;
35725
+ worker.associationDir = associationDir;
35726
+ worker.files = [];
35727
+ worker.fileSizes = [];
35728
+ worker.startAt = Date.now();
35729
+ this.pipeConnection(worker, remoteSocket);
35730
+ void this.replenishPool();
35731
+ }
35732
+ /** Finds an idle worker, retrying up to connectionTimeoutMs. */
35733
+ async findIdleWorker() {
35734
+ const idle = this.getIdleWorker();
35735
+ if (idle !== void 0) return idle;
35736
+ const maxRetries = Math.min(Math.ceil(this.connectionTimeoutMs / CONNECTION_RETRY_INTERVAL_MS), MAX_CONNECTION_RETRIES);
35737
+ for (let i = 0; i < maxRetries; i++) {
35738
+ await delay(CONNECTION_RETRY_INTERVAL_MS);
35739
+ const found = this.getIdleWorker();
35740
+ if (found !== void 0) return found;
35741
+ if (this.stopping) return void 0;
35742
+ }
35743
+ return void 0;
35744
+ }
35745
+ /** Returns the first idle worker, or undefined. */
35746
+ getIdleWorker() {
35747
+ for (const w of this.workers.values()) {
35748
+ if (w.state === "idle") return w;
35749
+ }
35750
+ return void 0;
35751
+ }
35752
+ /** Pipes remote socket bidirectionally to the worker's port. */
35753
+ pipeConnection(worker, remoteSocket) {
35754
+ const workerSocket = net__namespace.createConnection({ port: worker.port, host: "127.0.0.1" });
35755
+ worker.remoteSocket = remoteSocket;
35756
+ worker.workerSocket = workerSocket;
35757
+ remoteSocket.pipe(workerSocket);
35758
+ workerSocket.pipe(remoteSocket);
35759
+ remoteSocket.resume();
35760
+ const cleanup = () => {
35761
+ remoteSocket.unpipe(workerSocket);
35762
+ workerSocket.unpipe(remoteSocket);
35763
+ if (!remoteSocket.destroyed) remoteSocket.destroy();
35764
+ if (!workerSocket.destroyed) workerSocket.destroy();
35765
+ };
35766
+ remoteSocket.on("error", cleanup);
35767
+ workerSocket.on("error", cleanup);
35768
+ remoteSocket.on("close", cleanup);
35769
+ workerSocket.on("close", cleanup);
35770
+ }
35771
+ // -----------------------------------------------------------------------
35772
+ // Worker pool management
35773
+ // -----------------------------------------------------------------------
35774
+ /** Spawns `count` new workers and adds them to the pool. */
35775
+ async spawnWorkers(count) {
35776
+ const promises = [];
35777
+ for (let i = 0; i < count; i++) {
35778
+ promises.push(this.spawnWorker());
35779
+ }
35780
+ const results = await Promise.all(promises);
35781
+ for (const result of results) {
35782
+ if (!result.ok) return err(result.error);
35783
+ }
35784
+ return ok(void 0);
35785
+ }
35786
+ /** Spawns a single Dcmrecv worker with an ephemeral port. */
35787
+ async spawnWorker() {
35788
+ const portResult = await allocatePort();
35789
+ if (!portResult.ok) return portResult;
35790
+ const port = portResult.value;
35791
+ const tempDir = path__namespace.join(os__namespace.tmpdir(), `dcmrecv-pool-${String(port)}-${String(Date.now())}`);
35792
+ const mkdirResult = await ensureDirectory(tempDir);
35793
+ if (!mkdirResult.ok) return mkdirResult;
35794
+ const createResult = Dcmrecv.create({
35795
+ port,
35796
+ aeTitle: this.options.aeTitle ?? "DCMRECV",
35797
+ outputDirectory: tempDir,
35798
+ configFile: this.options.configFile,
35799
+ configProfile: this.options.configProfile
35800
+ });
35801
+ if (!createResult.ok) return createResult;
35802
+ const dcmrecv = createResult.value;
35803
+ const worker = {
35804
+ dcmrecv,
35805
+ port,
35806
+ tempDir,
35807
+ state: "idle",
35808
+ associationId: void 0,
35809
+ associationDir: void 0,
35810
+ files: [],
35811
+ fileSizes: [],
35812
+ startAt: void 0,
35813
+ remoteSocket: void 0,
35814
+ workerSocket: void 0
35815
+ };
35816
+ this.wireWorkerEvents(worker);
35817
+ const startResult = await dcmrecv.start();
35818
+ if (!startResult.ok) {
35819
+ return err(new Error(`DicomReceiver: worker start failed on port ${String(port)}: ${startResult.error.message}`));
35820
+ }
35821
+ this.workers.set(port, worker);
35822
+ return ok(worker);
35823
+ }
35824
+ /** Stops a single worker: stop process, clean temp dir, remove from pool. */
35825
+ async stopWorker(worker) {
35826
+ if (worker.remoteSocket !== void 0 && !worker.remoteSocket.destroyed) {
35827
+ worker.remoteSocket.destroy();
35828
+ }
35829
+ if (worker.workerSocket !== void 0 && !worker.workerSocket.destroyed) {
35830
+ worker.workerSocket.destroy();
35831
+ }
35832
+ await worker.dcmrecv.stop();
35833
+ worker.dcmrecv[Symbol.dispose]();
35834
+ await removeDirSafe(worker.tempDir);
35835
+ this.workers.delete(worker.port);
35836
+ }
35837
+ /** Pre-emptively spawns workers to keep idle count >= minPoolSize. */
35838
+ async replenishPool() {
35839
+ const status = this.poolStatus;
35840
+ const needed = this.minPoolSize - status.idle;
35841
+ const capacity = this.maxPoolSize - status.total;
35842
+ const toSpawn = Math.min(needed, capacity);
35843
+ if (toSpawn <= 0) return;
35844
+ const promises = [];
35845
+ for (let i = 0; i < toSpawn; i++) {
35846
+ promises.push(this.spawnWorker());
35847
+ }
35848
+ const results = await Promise.all(promises);
35849
+ for (const result of results) {
35850
+ if (!result.ok) {
35851
+ this.emit("error", { error: result.error });
35852
+ }
35853
+ }
35854
+ }
35855
+ /** Stops excess idle workers when idle count > minPoolSize + 2. */
35856
+ async scaleDown() {
35857
+ const idleWorkers = [];
35858
+ for (const w of this.workers.values()) {
35859
+ if (w.state === "idle") idleWorkers.push(w);
35860
+ }
35861
+ const excess = idleWorkers.length - (this.minPoolSize + 2);
35862
+ if (excess <= 0) return;
35863
+ const toStop = idleWorkers.slice(0, excess);
35864
+ const promises = [];
35865
+ for (const w of toStop) {
35866
+ promises.push(this.stopWorker(w));
35867
+ }
35868
+ await Promise.all(promises);
35869
+ }
35870
+ // -----------------------------------------------------------------------
35871
+ // Worker event wiring
35872
+ // -----------------------------------------------------------------------
35873
+ /** Wires FILE_RECEIVED and ASSOCIATION_COMPLETE events on a worker. */
35874
+ wireWorkerEvents(worker) {
35875
+ this.wireFileReceived(worker);
35876
+ this.wireAssociationComplete(worker);
35877
+ }
35878
+ /** Wires FILE_RECEIVED from dcmrecv worker to handleFileReceived. */
35879
+ wireFileReceived(worker) {
35880
+ worker.dcmrecv.onFileReceived((data) => {
35881
+ void this.handleFileReceived(worker, data);
35882
+ });
35883
+ }
35884
+ /** Moves a received file, opens it as DicomInstance, and emits FILE_RECEIVED. */
35885
+ async handleFileReceived(worker, data) {
35886
+ if (worker.associationDir === void 0 || worker.associationId === void 0) return;
35887
+ const srcPath = data.filePath;
35888
+ const destPath = path__namespace.join(worker.associationDir, path__namespace.basename(srcPath));
35889
+ const assocId = worker.associationId;
35890
+ const assocDir = worker.associationDir;
35891
+ const moveResult = await moveFile(srcPath, destPath);
35892
+ const finalPath = moveResult.ok ? destPath : srcPath;
35893
+ worker.files.push(finalPath);
35894
+ worker.fileSizes.push(await statFileSafe(finalPath));
35895
+ const openResult = await DicomInstance.open(finalPath);
35896
+ if (!openResult.ok) {
35897
+ this.emit("error", {
35898
+ error: openResult.error,
35899
+ filePath: finalPath,
35900
+ associationId: assocId,
35901
+ associationDir: assocDir,
35902
+ callingAE: data.callingAE,
35903
+ calledAE: data.calledAE,
35904
+ source: data.source
35905
+ });
35906
+ return;
35907
+ }
35908
+ this.emit("FILE_RECEIVED", {
35909
+ filePath: finalPath,
35910
+ associationId: assocId,
35911
+ associationDir: assocDir,
35912
+ callingAE: data.callingAE,
35913
+ calledAE: data.calledAE,
35914
+ source: data.source,
35915
+ instance: openResult.value
35916
+ });
35917
+ }
35918
+ /** Returns worker to idle pool on association complete, emits summary. */
35919
+ wireAssociationComplete(worker) {
35920
+ worker.dcmrecv.onAssociationComplete((data) => {
35921
+ const assocId = worker.associationId ?? data.associationId;
35922
+ const assocDir = worker.associationDir ?? "";
35923
+ const files = [...worker.files];
35924
+ const endAt = Date.now();
35925
+ const startAt = worker.startAt ?? endAt;
35926
+ const totalBytes = sumArray(worker.fileSizes);
35927
+ const elapsedMs = endAt - startAt;
35928
+ const bytesPerSecond = elapsedMs > 0 ? Math.round(totalBytes / elapsedMs * 1e3) : 0;
35929
+ this.emit("ASSOCIATION_COMPLETE", {
35930
+ associationId: assocId,
35931
+ associationDir: assocDir,
35932
+ callingAE: data.callingAE,
35933
+ calledAE: data.calledAE,
35934
+ source: data.source,
35935
+ files,
35936
+ durationMs: data.durationMs,
35937
+ endReason: data.endReason,
35938
+ totalBytes,
35939
+ bytesPerSecond,
35940
+ startAt,
35941
+ endAt
35942
+ });
35943
+ worker.state = "idle";
35944
+ worker.associationId = void 0;
35945
+ worker.associationDir = void 0;
35946
+ worker.files = [];
35947
+ worker.fileSizes = [];
35948
+ worker.startAt = void 0;
35949
+ worker.remoteSocket = void 0;
35950
+ worker.workerSocket = void 0;
35951
+ void this.scaleDown();
35952
+ });
35953
+ }
35954
+ // -----------------------------------------------------------------------
35955
+ // Abort signal
35956
+ // -----------------------------------------------------------------------
35957
+ /** Wires an AbortSignal to stop the receiver. */
35958
+ wireAbortSignal(signal) {
35959
+ if (signal.aborted) {
35960
+ void this.stop();
35961
+ return;
35962
+ }
35963
+ this.abortHandler = () => {
35964
+ void this.stop();
35965
+ };
35966
+ signal.addEventListener("abort", this.abortHandler, { once: true });
35967
+ }
35968
+ };
35969
+ async function ensureDirectory(dirPath) {
35970
+ try {
35971
+ await fs__namespace.mkdir(dirPath, { recursive: true });
35972
+ return ok(void 0);
35973
+ } catch (e) {
35974
+ const msg = e instanceof Error ? e.message : String(e);
35975
+ return err(new Error(`Failed to create directory ${dirPath}: ${msg}`));
35976
+ }
35977
+ }
35978
+ async function moveFile(src, dest) {
35979
+ try {
35980
+ await fs__namespace.rename(src, dest);
35981
+ return ok(void 0);
35982
+ } catch {
35983
+ try {
35984
+ await fs__namespace.copyFile(src, dest);
35985
+ await fs__namespace.unlink(src);
35986
+ return ok(void 0);
35987
+ } catch (e) {
35988
+ const msg = e instanceof Error ? e.message : String(e);
35989
+ return err(new Error(`Failed to move file ${src} \u2192 ${dest}: ${msg}`));
35990
+ }
35991
+ }
35992
+ }
35993
+ async function statFileSafe(filePath) {
35994
+ try {
35995
+ const stat3 = await fs__namespace.stat(filePath);
35996
+ return stat3.size;
35997
+ } catch {
35998
+ return 0;
35999
+ }
36000
+ }
36001
+ function sumArray(arr) {
36002
+ let total = 0;
36003
+ for (let i = 0; i < arr.length; i++) {
36004
+ total += arr[i] ?? 0;
36005
+ }
36006
+ return total;
36007
+ }
36008
+ async function removeDirSafe(dirPath) {
36009
+ try {
36010
+ await fs__namespace.rm(dirPath, { recursive: true, force: true });
36011
+ } catch {
36012
+ }
36013
+ }
36014
+ function delay(ms) {
36015
+ return new Promise((resolve) => {
36016
+ setTimeout(resolve, ms);
36017
+ });
36018
+ }
34981
36019
 
34982
36020
  // src/pacs/types.ts
34983
36021
  var QueryLevel = {
@@ -35060,11 +36098,11 @@ function addFilterKeys(keys, filter) {
35060
36098
  for (let i = 0; i < fields.length; i += 1) {
35061
36099
  const field = fields[i];
35062
36100
  if (field === void 0) continue;
35063
- const tag = FILTER_TAG_MAP[field];
35064
- if (tag === void 0) continue;
36101
+ const tag2 = FILTER_TAG_MAP[field];
36102
+ if (tag2 === void 0) continue;
35065
36103
  const value = filter[field];
35066
36104
  if (typeof value !== "string") continue;
35067
- keys.push(`${tag}=${value}`);
36105
+ keys.push(`${tag2}=${value}`);
35068
36106
  }
35069
36107
  }
35070
36108
  function extractTag(key) {
@@ -35073,11 +36111,11 @@ function extractTag(key) {
35073
36111
  }
35074
36112
  function addReturnKeys(keys, returnKeys) {
35075
36113
  for (let i = 0; i < returnKeys.length; i += 1) {
35076
- const tag = returnKeys[i];
35077
- if (tag === void 0) continue;
35078
- const alreadyPresent = keys.some((k) => extractTag(k) === tag);
36114
+ const tag2 = returnKeys[i];
36115
+ if (tag2 === void 0) continue;
36116
+ const alreadyPresent = keys.some((k) => extractTag(k) === tag2);
35079
36117
  if (!alreadyPresent) {
35080
- keys.push(`${tag}=`);
36118
+ keys.push(`${tag2}=`);
35081
36119
  }
35082
36120
  }
35083
36121
  }
@@ -35105,13 +36143,13 @@ function buildWorklistKeys(filter) {
35105
36143
  var MAX_RESPONSE_FILES = 1e4;
35106
36144
  async function createTempDir() {
35107
36145
  return stderrLib.tryCatch(
35108
- () => promises.mkdtemp(path.join(os.tmpdir(), "dcmtk-pacs-")),
36146
+ () => fs.mkdtemp(path.join(os.tmpdir(), "dcmtk-pacs-")),
35109
36147
  (e) => new Error(`Failed to create temp directory: ${e.message}`)
35110
36148
  );
35111
36149
  }
35112
36150
  async function listDcmFiles(directory) {
35113
36151
  try {
35114
- const entries = await promises.readdir(directory);
36152
+ const entries = await fs.readdir(directory);
35115
36153
  const dcmFiles = [];
35116
36154
  for (let i = 0; i < entries.length; i += 1) {
35117
36155
  const entry = entries[i];
@@ -35129,7 +36167,7 @@ async function listDcmFiles(directory) {
35129
36167
  }
35130
36168
  async function cleanupTempDir(directory) {
35131
36169
  try {
35132
- await promises.rm(directory, { recursive: true, force: true });
36170
+ await fs.rm(directory, { recursive: true, force: true });
35133
36171
  } catch {
35134
36172
  }
35135
36173
  }
@@ -35588,6 +36626,7 @@ async function retry(operation, options) {
35588
36626
  }
35589
36627
 
35590
36628
  exports.AETitleSchema = AETitleSchema;
36629
+ exports.AssociationTracker = AssociationTracker;
35591
36630
  exports.ChangeSet = ChangeSet;
35592
36631
  exports.ColorConversion = ColorConversion;
35593
36632
  exports.DCMPRSCP_FATAL_EVENTS = DCMPRSCP_FATAL_EVENTS;
@@ -35618,7 +36657,8 @@ exports.Dcmrecv = Dcmrecv;
35618
36657
  exports.DcmrecvEvent = DcmrecvEvent;
35619
36658
  exports.DcmtkProcess = DcmtkProcess;
35620
36659
  exports.DicomDataset = DicomDataset;
35621
- exports.DicomFile = DicomFile;
36660
+ exports.DicomInstance = DicomInstance;
36661
+ exports.DicomReceiver = DicomReceiver;
35622
36662
  exports.DicomTagPathSchema = DicomTagPathSchema;
35623
36663
  exports.DicomTagSchema = DicomTagSchema;
35624
36664
  exports.FilenameMode = FilenameMode;
@@ -35637,6 +36677,7 @@ exports.PacsClient = PacsClient;
35637
36677
  exports.PortSchema = PortSchema;
35638
36678
  exports.PreferredTransferSyntax = PreferredTransferSyntax;
35639
36679
  exports.ProcessState = ProcessState;
36680
+ exports.ProposedTransferSyntax = ProposedTransferSyntax;
35640
36681
  exports.QueryLevel = QueryLevel;
35641
36682
  exports.QueryModel = QueryModel;
35642
36683
  exports.REQUIRED_BINARIES = REQUIRED_BINARIES;
@@ -35742,9 +36783,9 @@ exports.sopClassNameFromUID = sopClassNameFromUID;
35742
36783
  exports.spawnCommand = spawnCommand;
35743
36784
  exports.stl2dcm = stl2dcm;
35744
36785
  exports.storescu = storescu;
36786
+ exports.tag = tag;
35745
36787
  exports.tagPathToSegments = tagPathToSegments;
35746
36788
  exports.termscu = termscu;
35747
- exports.unwrap = unwrap;
35748
36789
  exports.xml2dcm = xml2dcm;
35749
36790
  exports.xml2dsr = xml2dsr;
35750
36791
  exports.xmlToJson = xmlToJson;