@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.js CHANGED
@@ -1,3 +1,4 @@
1
+ import * as path from 'path';
1
2
  import { normalize, join } from 'path';
2
3
  import { z } from 'zod';
3
4
  import { existsSync } from 'fs';
@@ -5,8 +6,11 @@ import { spawn, execSync } from 'child_process';
5
6
  import kill from 'tree-kill';
6
7
  import { EventEmitter } from 'events';
7
8
  import { stderr, tryCatch } from 'stderr-lib';
8
- import { copyFile, unlink, stat, mkdtemp, rm, readdir } from 'fs/promises';
9
9
  import { XMLParser } from 'fast-xml-parser';
10
+ import * as fs from 'fs/promises';
11
+ import { copyFile, unlink, stat, mkdtemp, rm, readdir } from 'fs/promises';
12
+ import * as net from 'net';
13
+ import * as os from 'os';
10
14
  import { tmpdir } from 'os';
11
15
 
12
16
  var __defProp = Object.defineProperty;
@@ -23,12 +27,6 @@ function err(error) {
23
27
  function assertUnreachable(x) {
24
28
  throw new Error(`Exhaustive check failed: ${JSON.stringify(x)}`);
25
29
  }
26
- function unwrap(result) {
27
- if (result.ok) {
28
- return result.value;
29
- }
30
- throw result.error;
31
- }
32
30
  function mapResult(result, fn) {
33
31
  if (result.ok) {
34
32
  return ok(fn(result.value));
@@ -118,6 +116,11 @@ function createPort(input) {
118
116
  }
119
117
  return ok(input);
120
118
  }
119
+ function tag(input) {
120
+ const result = createDicomTagPath(input);
121
+ if (!result.ok) throw result.error;
122
+ return result.value;
123
+ }
121
124
  var AETitleSchema = z.string().min(AE_TITLE_MIN_LENGTH).max(AE_TITLE_MAX_LENGTH).regex(AE_TITLE_PATTERN);
122
125
  var PortSchema = z.number().int().min(PORT_MIN).max(PORT_MAX);
123
126
  var DicomTagSchema = z.string().regex(DICOM_TAG_PATTERN);
@@ -30253,8 +30256,8 @@ var dictionary_default = {
30253
30256
 
30254
30257
  // src/dicom/dictionary.ts
30255
30258
  var dictionary = dictionary_default;
30256
- function lookupTag(tag) {
30257
- const key = tag.includes(",") ? tag.replace(/[(),]/g, "") : tag;
30259
+ function lookupTag(tag2) {
30260
+ const key = tag2.includes(",") ? tag2.replace(/[(),]/g, "") : tag2;
30258
30261
  return dictionary[key.toUpperCase()];
30259
30262
  }
30260
30263
  var nameIndex;
@@ -30421,13 +30424,13 @@ function advancePastMatch(remaining, matchLength) {
30421
30424
  if (after.length === 1) throw new Error("Tag path cannot end with a dot separator");
30422
30425
  return after.slice(1);
30423
30426
  }
30424
- function tagPathToSegments(path) {
30427
+ function tagPathToSegments(path2) {
30425
30428
  const segments = [];
30426
- let remaining = path;
30429
+ let remaining = path2;
30427
30430
  for (let i = 0; i < MAX_TRAVERSAL_DEPTH && remaining.length > 0; i++) {
30428
30431
  const match = SEGMENT_PATTERN.exec(remaining);
30429
30432
  if (match === null) {
30430
- throw new Error(`Invalid tag path segment at position ${path.length - remaining.length}: "${remaining}"`);
30433
+ throw new Error(`Invalid tag path segment at position ${path2.length - remaining.length}: "${remaining}"`);
30431
30434
  }
30432
30435
  segments.push(matchToSegment(match));
30433
30436
  remaining = advancePastMatch(remaining, match[0].length);
@@ -30468,18 +30471,18 @@ function isValidDicomJsonModel(obj) {
30468
30471
  }
30469
30472
  var TAG_WITH_PARENS = /^\(([0-9A-Fa-f]{4}),([0-9A-Fa-f]{4})\)$/;
30470
30473
  var TAG_HEX_ONLY = /^[0-9A-Fa-f]{8}$/;
30471
- function normalizeTag(tag) {
30472
- const parenMatch = TAG_WITH_PARENS.exec(tag);
30474
+ function normalizeTag(tag2) {
30475
+ const parenMatch = TAG_WITH_PARENS.exec(tag2);
30473
30476
  if (parenMatch !== null) {
30474
30477
  const group = parenMatch[1];
30475
30478
  const element = parenMatch[2];
30476
- if (group === void 0 || element === void 0) return err(new Error(`Invalid tag: "${tag}"`));
30479
+ if (group === void 0 || element === void 0) return err(new Error(`Invalid tag: "${tag2}"`));
30477
30480
  return ok(`${group}${element}`.toUpperCase());
30478
30481
  }
30479
- if (TAG_HEX_ONLY.test(tag)) {
30480
- return ok(tag.toUpperCase());
30482
+ if (TAG_HEX_ONLY.test(tag2)) {
30483
+ return ok(tag2.toUpperCase());
30481
30484
  }
30482
- return err(new Error(`Invalid tag format: "${tag}". Expected (XXXX,XXXX) or XXXXXXXX`));
30485
+ return err(new Error(`Invalid tag format: "${tag2}". Expected (XXXX,XXXX) or XXXXXXXX`));
30483
30486
  }
30484
30487
  function resolveElement(data, tagKey) {
30485
30488
  return data[tagKey];
@@ -30660,11 +30663,11 @@ var DicomDataset = class _DicomDataset {
30660
30663
  * @param tag - A DicomTag `(0010,0010)` or hex string `00100010`
30661
30664
  * @returns Result containing the element or an error if not found
30662
30665
  */
30663
- getElement(tag) {
30664
- const norm = normalizeTag(tag);
30666
+ getElement(tag2) {
30667
+ const norm = normalizeTag(tag2);
30665
30668
  if (!norm.ok) return err(norm.error);
30666
30669
  const element = resolveElement(this.data, norm.value);
30667
- if (element === void 0) return err(new Error(`Tag ${tag} not found`));
30670
+ if (element === void 0) return err(new Error(`Tag ${tag2} not found`));
30668
30671
  return ok(element);
30669
30672
  }
30670
30673
  /**
@@ -30673,11 +30676,11 @@ var DicomDataset = class _DicomDataset {
30673
30676
  * @param tag - A DicomTag `(0010,0010)` or hex string `00100010`
30674
30677
  * @returns Result containing the readonly Value array or an error
30675
30678
  */
30676
- getValue(tag) {
30677
- const elemResult = this.getElement(tag);
30679
+ getValue(tag2) {
30680
+ const elemResult = this.getElement(tag2);
30678
30681
  if (!elemResult.ok) return err(elemResult.error);
30679
30682
  const values = elemResult.value.Value;
30680
- if (values === void 0) return err(new Error(`Tag ${tag} has no Value`));
30683
+ if (values === void 0) return err(new Error(`Tag ${tag2} has no Value`));
30681
30684
  return ok(values);
30682
30685
  }
30683
30686
  /**
@@ -30686,11 +30689,11 @@ var DicomDataset = class _DicomDataset {
30686
30689
  * @param tag - A DicomTag `(0010,0010)` or hex string `00100010`
30687
30690
  * @returns Result containing the first value or an error
30688
30691
  */
30689
- getFirstValue(tag) {
30690
- const valResult = this.getValue(tag);
30692
+ getFirstValue(tag2) {
30693
+ const valResult = this.getValue(tag2);
30691
30694
  if (!valResult.ok) return err(valResult.error);
30692
30695
  const first = valResult.value[0];
30693
- if (first === void 0) return err(new Error(`Tag ${tag} Value array is empty`));
30696
+ if (first === void 0) return err(new Error(`Tag ${tag2} Value array is empty`));
30694
30697
  return ok(first);
30695
30698
  }
30696
30699
  /**
@@ -30703,8 +30706,8 @@ var DicomDataset = class _DicomDataset {
30703
30706
  * @param fallback - Value to return if tag is missing (default: `''`)
30704
30707
  * @returns The string value or the fallback
30705
30708
  */
30706
- getString(tag, fallback = "") {
30707
- const elemResult = this.getElement(tag);
30709
+ getString(tag2, fallback = "") {
30710
+ const elemResult = this.getElement(tag2);
30708
30711
  if (!elemResult.ok) return fallback;
30709
30712
  const str = extractString(elemResult.value);
30710
30713
  return str.length > 0 ? str : fallback;
@@ -30715,8 +30718,8 @@ var DicomDataset = class _DicomDataset {
30715
30718
  * @param tag - A DicomTag `(0010,0010)` or hex string `00100010`
30716
30719
  * @returns Result containing the number or an error
30717
30720
  */
30718
- getNumber(tag) {
30719
- const elemResult = this.getElement(tag);
30721
+ getNumber(tag2) {
30722
+ const elemResult = this.getElement(tag2);
30720
30723
  if (!elemResult.ok) return err(elemResult.error);
30721
30724
  return extractNumber(elemResult.value);
30722
30725
  }
@@ -30728,8 +30731,8 @@ var DicomDataset = class _DicomDataset {
30728
30731
  * @param tag - A DicomTag `(0010,0010)` or hex string `00100010`
30729
30732
  * @returns Result containing the readonly string array or an error
30730
30733
  */
30731
- getStrings(tag) {
30732
- const elemResult = this.getElement(tag);
30734
+ getStrings(tag2) {
30735
+ const elemResult = this.getElement(tag2);
30733
30736
  if (!elemResult.ok) return err(elemResult.error);
30734
30737
  return ok(extractStrings(elemResult.value));
30735
30738
  }
@@ -30739,8 +30742,8 @@ var DicomDataset = class _DicomDataset {
30739
30742
  * @param tag - A DicomTag `(0010,0010)` or hex string `00100010`
30740
30743
  * @returns `true` if the tag is present
30741
30744
  */
30742
- hasTag(tag) {
30743
- const norm = normalizeTag(tag);
30745
+ hasTag(tag2) {
30746
+ const norm = normalizeTag(tag2);
30744
30747
  if (!norm.ok) return false;
30745
30748
  return resolveElement(this.data, norm.value) !== void 0;
30746
30749
  }
@@ -30750,10 +30753,10 @@ var DicomDataset = class _DicomDataset {
30750
30753
  * @param path - A branded DicomTagPath, e.g. `(0040,A730)[0].(0040,A160)`
30751
30754
  * @returns Result containing the element at the path or an error
30752
30755
  */
30753
- getElementAtPath(path) {
30756
+ getElementAtPath(path2) {
30754
30757
  let segments;
30755
30758
  try {
30756
- segments = tagPathToSegments(path);
30759
+ segments = tagPathToSegments(path2);
30757
30760
  } catch (e) {
30758
30761
  return err(stderr(e));
30759
30762
  }
@@ -30770,10 +30773,10 @@ var DicomDataset = class _DicomDataset {
30770
30773
  * @param path - A branded DicomTagPath, e.g. `(0040,A730)[*].(0040,A160)`
30771
30774
  * @returns A readonly array of all matching values (may be empty)
30772
30775
  */
30773
- findValues(path) {
30776
+ findValues(path2) {
30774
30777
  let segments;
30775
30778
  try {
30776
- segments = tagPathToSegments(path);
30779
+ segments = tagPathToSegments(path2);
30777
30780
  } catch {
30778
30781
  return [];
30779
30782
  }
@@ -30856,6 +30859,18 @@ function buildMergedModifications(base, other, erasures) {
30856
30859
  }
30857
30860
  return merged;
30858
30861
  }
30862
+ function applyBatchEntries(initial, entries) {
30863
+ const keys = Object.keys(entries);
30864
+ let cs = initial;
30865
+ for (let i = 0; i < keys.length; i++) {
30866
+ const key = keys[i];
30867
+ if (key === void 0) continue;
30868
+ const value = entries[key];
30869
+ if (value === void 0) continue;
30870
+ cs = cs.setTag(key, value);
30871
+ }
30872
+ return cs;
30873
+ }
30859
30874
  var ChangeSet = class _ChangeSet {
30860
30875
  constructor(mods, erasures) {
30861
30876
  __publicField(this, "mods");
@@ -30873,22 +30888,22 @@ var ChangeSet = class _ChangeSet {
30873
30888
  * Control characters (except LF/CR) are stripped from the value.
30874
30889
  * If the tag was previously erased, it is removed from the erasure set.
30875
30890
  *
30876
- * @param path - The DICOM tag path to set
30891
+ * @param path - The DICOM tag path to set (e.g. `'(0010,0010)'`)
30877
30892
  * @param value - The new value for the tag
30878
30893
  * @returns A new ChangeSet with the modification applied
30879
30894
  * @throws Error if operation count would exceed MAX_CHANGESET_OPERATIONS
30880
30895
  */
30881
- setTag(path, value) {
30896
+ setTag(path2, value) {
30882
30897
  const totalOps = this.mods.size + this.erased.size;
30883
30898
  if (totalOps >= MAX_CHANGESET_OPERATIONS) {
30884
30899
  throw new Error(`ChangeSet operation limit (${MAX_CHANGESET_OPERATIONS}) exceeded`);
30885
30900
  }
30886
- tagPathToSegments(path);
30901
+ tagPathToSegments(path2);
30887
30902
  const sanitized = sanitizeValue(value);
30888
30903
  const newMods = new Map(this.mods);
30889
- newMods.set(path, sanitized);
30904
+ newMods.set(path2, sanitized);
30890
30905
  const newErasures = new Set(this.erased);
30891
- newErasures.delete(path);
30906
+ newErasures.delete(path2);
30892
30907
  return new _ChangeSet(newMods, newErasures);
30893
30908
  }
30894
30909
  /**
@@ -30896,20 +30911,20 @@ var ChangeSet = class _ChangeSet {
30896
30911
  *
30897
30912
  * If the tag was previously set, the modification is removed.
30898
30913
  *
30899
- * @param path - The DICOM tag path to erase
30914
+ * @param path - The DICOM tag path to erase (e.g. `'(0010,0010)'`)
30900
30915
  * @returns A new ChangeSet with the erasure applied
30901
30916
  * @throws Error if operation count would exceed MAX_CHANGESET_OPERATIONS
30902
30917
  */
30903
- eraseTag(path) {
30918
+ eraseTag(path2) {
30904
30919
  const totalOps = this.mods.size + this.erased.size;
30905
30920
  if (totalOps >= MAX_CHANGESET_OPERATIONS) {
30906
30921
  throw new Error(`ChangeSet operation limit (${MAX_CHANGESET_OPERATIONS}) exceeded`);
30907
30922
  }
30908
- tagPathToSegments(path);
30923
+ tagPathToSegments(path2);
30909
30924
  const newMods = new Map(this.mods);
30910
- newMods.delete(path);
30925
+ newMods.delete(path2);
30911
30926
  const newErasures = new Set(this.erased);
30912
- newErasures.add(path);
30927
+ newErasures.add(path2);
30913
30928
  return new _ChangeSet(newMods, newErasures);
30914
30929
  }
30915
30930
  /**
@@ -30927,6 +30942,50 @@ var ChangeSet = class _ChangeSet {
30927
30942
  newErasures.add(ERASE_PRIVATE_SENTINEL);
30928
30943
  return new _ChangeSet(new Map(this.mods), newErasures);
30929
30944
  }
30945
+ // -----------------------------------------------------------------------
30946
+ // Convenience setters for common DICOM tags
30947
+ // -----------------------------------------------------------------------
30948
+ /** Sets Patient's Name (0010,0010). */
30949
+ setPatientName(value) {
30950
+ return this.setTag("(0010,0010)", value);
30951
+ }
30952
+ /** Sets Patient ID (0010,0020). */
30953
+ setPatientID(value) {
30954
+ return this.setTag("(0010,0020)", value);
30955
+ }
30956
+ /** Sets Study Date (0008,0020). */
30957
+ setStudyDate(value) {
30958
+ return this.setTag("(0008,0020)", value);
30959
+ }
30960
+ /** Sets Modality (0008,0060). */
30961
+ setModality(value) {
30962
+ return this.setTag("(0008,0060)", value);
30963
+ }
30964
+ /** Sets Accession Number (0008,0050). */
30965
+ setAccessionNumber(value) {
30966
+ return this.setTag("(0008,0050)", value);
30967
+ }
30968
+ /** Sets Study Description (0008,1030). */
30969
+ setStudyDescription(value) {
30970
+ return this.setTag("(0008,1030)", value);
30971
+ }
30972
+ /** Sets Series Description (0008,103E). */
30973
+ setSeriesDescription(value) {
30974
+ return this.setTag("(0008,103E)", value);
30975
+ }
30976
+ /** Sets Institution Name (0008,0080). */
30977
+ setInstitutionName(value) {
30978
+ return this.setTag("(0008,0080)", value);
30979
+ }
30980
+ /**
30981
+ * Sets multiple tags at once, returning a new ChangeSet.
30982
+ *
30983
+ * @param entries - A record of tag path → value pairs
30984
+ * @returns A new ChangeSet with all modifications applied
30985
+ */
30986
+ setBatch(entries) {
30987
+ return applyBatchEntries(this, entries);
30988
+ }
30930
30989
  /** All pending tag modifications as a readonly map of path → value. */
30931
30990
  get modifications() {
30932
30991
  return this.mods;
@@ -30969,8 +31028,8 @@ var ChangeSet = class _ChangeSet {
30969
31028
  */
30970
31029
  toModifications() {
30971
31030
  const result = [];
30972
- for (const [tag, value] of this.mods) {
30973
- result.push({ tag, value });
31031
+ for (const [tag2, value] of this.mods) {
31032
+ result.push({ tag: tag2, value });
30974
31033
  }
30975
31034
  return result;
30976
31035
  }
@@ -30984,9 +31043,9 @@ var ChangeSet = class _ChangeSet {
30984
31043
  */
30985
31044
  toErasureArgs() {
30986
31045
  const result = [];
30987
- for (const path of this.erased) {
30988
- if (path !== ERASE_PRIVATE_SENTINEL) {
30989
- result.push(path);
31046
+ for (const path2 of this.erased) {
31047
+ if (path2 !== ERASE_PRIVATE_SENTINEL) {
31048
+ result.push(path2);
30990
31049
  }
30991
31050
  }
30992
31051
  return result;
@@ -31028,12 +31087,72 @@ function createValidationError(toolName, zodError) {
31028
31087
  for (let i = 0; i < zodError.issues.length; i++) {
31029
31088
  const issue = zodError.issues[i];
31030
31089
  if (issue === void 0) continue;
31031
- const path = issue.path.length > 0 ? issue.path.map(String).join(".") : "(root)";
31032
- parts.push(`${path}: ${issue.message}`);
31090
+ const path2 = issue.path.length > 0 ? issue.path.map(String).join(".") : "(root)";
31091
+ parts.push(`${path2}: ${issue.message}`);
31033
31092
  }
31034
31093
  const detail = parts.length > 0 ? parts.join("; ") : "unknown validation error";
31035
31094
  return new Error(`${toolName}: invalid options \u2014 ${detail}`);
31036
31095
  }
31096
+
31097
+ // src/tools/dcm2xml.ts
31098
+ var Dcm2xmlCharset = {
31099
+ /** Use UTF-8 encoding (default). */
31100
+ UTF8: "utf8",
31101
+ /** Use Latin-1 encoding. */
31102
+ LATIN1: "latin1",
31103
+ /** Use ASCII encoding. */
31104
+ ASCII: "ascii"
31105
+ };
31106
+ var Dcm2xmlOptionsSchema = z.object({
31107
+ timeoutMs: z.number().int().positive().optional(),
31108
+ signal: z.instanceof(AbortSignal).optional(),
31109
+ namespace: z.boolean().optional(),
31110
+ charset: z.enum(["utf8", "latin1", "ascii"]).optional(),
31111
+ writeBinaryData: z.boolean().optional(),
31112
+ encodeBinaryBase64: z.boolean().optional()
31113
+ }).strict().optional();
31114
+ function buildArgs(inputPath, options) {
31115
+ const args = [];
31116
+ if (options?.namespace === true) {
31117
+ args.push("+Xn");
31118
+ }
31119
+ if (options?.charset === "latin1") {
31120
+ args.push("+Cl");
31121
+ } else if (options?.charset === "ascii") {
31122
+ args.push("+Ca");
31123
+ }
31124
+ if (options?.writeBinaryData === true) {
31125
+ args.push("+Wb");
31126
+ if (options.encodeBinaryBase64 !== false) {
31127
+ args.push("+Eb");
31128
+ }
31129
+ }
31130
+ args.push(inputPath);
31131
+ return args;
31132
+ }
31133
+ async function dcm2xml(inputPath, options) {
31134
+ const validation = Dcm2xmlOptionsSchema.safeParse(options);
31135
+ if (!validation.success) {
31136
+ return err(createValidationError("dcm2xml", validation.error));
31137
+ }
31138
+ const binaryResult = resolveBinary("dcm2xml");
31139
+ if (!binaryResult.ok) {
31140
+ return err(binaryResult.error);
31141
+ }
31142
+ const args = buildArgs(inputPath, options);
31143
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
31144
+ const result = await execCommand(binaryResult.value, args, {
31145
+ timeoutMs,
31146
+ signal: options?.signal
31147
+ });
31148
+ if (!result.ok) {
31149
+ return err(result.error);
31150
+ }
31151
+ if (result.value.exitCode !== 0) {
31152
+ return err(createToolError("dcm2xml", args, result.value.exitCode, result.value.stderr));
31153
+ }
31154
+ return ok({ xml: result.value.stdout });
31155
+ }
31037
31156
  var PN_REPS = ["Alphabetic", "Ideographic", "Phonetic"];
31038
31157
  var ARRAY_TAG_NAMES = /* @__PURE__ */ new Set(["DicomAttribute", "Value", "PersonName", "Item"]);
31039
31158
  var KNOWN_VR_CODES = /* @__PURE__ */ new Set([
@@ -31169,9 +31288,9 @@ function convertAttributes(obj) {
31169
31288
  for (const attr of attrs) {
31170
31289
  if (typeof attr !== "object" || attr === null) continue;
31171
31290
  const xmlAttr = attr;
31172
- const tag = xmlAttr["@_tag"];
31173
- if (tag === void 0) continue;
31174
- result[tag] = convertElement(xmlAttr);
31291
+ const tag2 = xmlAttr["@_tag"];
31292
+ if (tag2 === void 0) continue;
31293
+ result[tag2] = convertElement(xmlAttr);
31175
31294
  }
31176
31295
  return result;
31177
31296
  }
@@ -31294,263 +31413,6 @@ async function dcm2json(inputPath, options) {
31294
31413
  }
31295
31414
  return tryDirectPath(inputPath, timeoutMs, signal);
31296
31415
  }
31297
- 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+\])?)*)?$/;
31298
- var TagModificationSchema = z.object({
31299
- tag: z.string().regex(TAG_OR_PATH_PATTERN),
31300
- value: z.string()
31301
- });
31302
- var DcmodifyOptionsSchema = z.object({
31303
- timeoutMs: z.number().int().positive().optional(),
31304
- signal: z.instanceof(AbortSignal).optional(),
31305
- modifications: z.array(TagModificationSchema).optional().default([]),
31306
- erasures: z.array(z.string()).optional(),
31307
- erasePrivateTags: z.boolean().optional(),
31308
- noBackup: z.boolean().optional(),
31309
- insertIfMissing: z.boolean().optional()
31310
- }).strict().refine((data) => data.modifications.length > 0 || data.erasures !== void 0 && data.erasures.length > 0 || data.erasePrivateTags === true, {
31311
- message: "At least one of modifications, erasures, or erasePrivateTags is required"
31312
- });
31313
- function buildArgs(inputPath, options) {
31314
- const args = [];
31315
- if (options.noBackup !== false) {
31316
- args.push("-nb");
31317
- }
31318
- const flag = options.insertIfMissing === true ? "-i" : "-m";
31319
- const modifications = options.modifications ?? [];
31320
- for (const mod of modifications) {
31321
- args.push(flag, `${mod.tag}=${mod.value}`);
31322
- }
31323
- if (options.erasures !== void 0) {
31324
- for (const erasure of options.erasures) {
31325
- args.push("-e", erasure);
31326
- }
31327
- }
31328
- if (options.erasePrivateTags === true) {
31329
- args.push("-ep");
31330
- }
31331
- args.push(inputPath);
31332
- return args;
31333
- }
31334
- async function dcmodify(inputPath, options) {
31335
- const validation = DcmodifyOptionsSchema.safeParse(options);
31336
- if (!validation.success) {
31337
- return err(new Error(`dcmodify: invalid options: ${validation.error.message}`));
31338
- }
31339
- const binaryResult = resolveBinary("dcmodify");
31340
- if (!binaryResult.ok) {
31341
- return err(binaryResult.error);
31342
- }
31343
- const args = buildArgs(inputPath, options);
31344
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
31345
- const result = await spawnCommand(binaryResult.value, args, {
31346
- timeoutMs,
31347
- signal: options.signal
31348
- });
31349
- if (!result.ok) {
31350
- return err(result.error);
31351
- }
31352
- if (result.value.exitCode !== 0) {
31353
- return err(createToolError("dcmodify", args, result.value.exitCode, result.value.stderr));
31354
- }
31355
- return ok({ filePath: inputPath });
31356
- }
31357
-
31358
- // src/dicom/DicomFile.ts
31359
- async function applyModifications(filePath, changeset, options) {
31360
- const modifications = changeset.toModifications();
31361
- const erasures = changeset.toErasureArgs();
31362
- const result = await dcmodify(filePath, {
31363
- modifications: modifications.length > 0 ? modifications : void 0,
31364
- erasures: erasures.length > 0 ? erasures : void 0,
31365
- erasePrivateTags: changeset.erasePrivate || void 0,
31366
- insertIfMissing: true,
31367
- timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
31368
- signal: options.signal
31369
- });
31370
- if (!result.ok) return err(result.error);
31371
- return ok(void 0);
31372
- }
31373
- async function copyFileSafe(source, dest) {
31374
- return tryCatch(
31375
- () => copyFile(source, dest),
31376
- (e) => new Error(`Failed to copy file: ${e.message}`)
31377
- );
31378
- }
31379
- async function statFileSize(path) {
31380
- return tryCatch(
31381
- async () => (await stat(path)).size,
31382
- (e) => new Error(`Failed to stat file: ${e.message}`)
31383
- );
31384
- }
31385
- async function unlinkFile(path) {
31386
- return tryCatch(
31387
- () => unlink(path),
31388
- (e) => new Error(`Failed to delete file: ${e.message}`)
31389
- );
31390
- }
31391
- var DicomFile = class _DicomFile {
31392
- constructor(dataset, filePath, changes) {
31393
- /** The immutable DICOM dataset read from the file. */
31394
- __publicField(this, "dataset");
31395
- /** The branded file path. */
31396
- __publicField(this, "filePath");
31397
- /** The accumulated pending changes. */
31398
- __publicField(this, "changes");
31399
- this.dataset = dataset;
31400
- this.filePath = filePath;
31401
- this.changes = changes;
31402
- }
31403
- /**
31404
- * Opens a DICOM file and reads its dataset.
31405
- *
31406
- * @param path - Filesystem path to the DICOM file
31407
- * @param options - Timeout and abort options
31408
- * @returns A Result containing the DicomFile or an error
31409
- */
31410
- static async open(path, options) {
31411
- const filePathResult = createDicomFilePath(path);
31412
- if (!filePathResult.ok) return err(filePathResult.error);
31413
- const jsonResult = await dcm2json(path, {
31414
- timeoutMs: options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
31415
- signal: options?.signal
31416
- });
31417
- if (!jsonResult.ok) return err(jsonResult.error);
31418
- const datasetResult = DicomDataset.fromJson(jsonResult.value.data);
31419
- if (!datasetResult.ok) return err(datasetResult.error);
31420
- return ok(new _DicomFile(datasetResult.value, filePathResult.value, ChangeSet.empty()));
31421
- }
31422
- /**
31423
- * Returns a new DicomFile with the given changes merged into the pending changes.
31424
- *
31425
- * @param changes - A ChangeSet to merge with existing pending changes
31426
- * @returns A new DicomFile with accumulated changes
31427
- */
31428
- withChanges(changes) {
31429
- return new _DicomFile(this.dataset, this.filePath, this.changes.merge(changes));
31430
- }
31431
- /**
31432
- * Returns a new DicomFile with a different file path.
31433
- *
31434
- * Preserves the dataset and pending changes.
31435
- *
31436
- * @param newPath - The new branded file path
31437
- * @returns A new DicomFile pointing to the new path
31438
- */
31439
- withFilePath(newPath) {
31440
- return new _DicomFile(this.dataset, newPath, this.changes);
31441
- }
31442
- /**
31443
- * Applies pending changes to the file in-place using dcmodify.
31444
- *
31445
- * If there are no pending changes, this is a no-op that returns success.
31446
- * After applying, the dataset is NOT refreshed — call {@link DicomFile.open}
31447
- * again if you need fresh data.
31448
- *
31449
- * @param options - Timeout and abort options
31450
- * @returns A Result indicating success or failure
31451
- */
31452
- async applyChanges(options) {
31453
- if (this.changes.isEmpty) return ok(void 0);
31454
- return applyModifications(this.filePath, this.changes, options ?? {});
31455
- }
31456
- /**
31457
- * Copies the file to a new path and applies pending changes to the copy.
31458
- *
31459
- * If there are no pending changes, only the copy is performed.
31460
- * On dcmodify failure, the copy is cleaned up.
31461
- *
31462
- * @param outputPath - Destination filesystem path
31463
- * @param options - Timeout and abort options
31464
- * @returns A Result containing the branded output path or an error
31465
- */
31466
- async writeAs(outputPath, options) {
31467
- const outPathResult = createDicomFilePath(outputPath);
31468
- if (!outPathResult.ok) return err(outPathResult.error);
31469
- const copyResult = await copyFileSafe(this.filePath, outputPath);
31470
- if (!copyResult.ok) return err(copyResult.error);
31471
- if (this.changes.isEmpty) return ok(outPathResult.value);
31472
- const applyResult = await applyModifications(outPathResult.value, this.changes, options ?? {});
31473
- if (!applyResult.ok) {
31474
- await unlinkFile(outputPath);
31475
- return err(applyResult.error);
31476
- }
31477
- return ok(outPathResult.value);
31478
- }
31479
- /**
31480
- * Gets the file size in bytes.
31481
- *
31482
- * @returns A Result containing the size or an error
31483
- */
31484
- async fileSize() {
31485
- return statFileSize(this.filePath);
31486
- }
31487
- /**
31488
- * Deletes the file from the filesystem.
31489
- *
31490
- * @returns A Result indicating success or failure
31491
- */
31492
- async unlink() {
31493
- return unlinkFile(this.filePath);
31494
- }
31495
- };
31496
- var Dcm2xmlCharset = {
31497
- /** Use UTF-8 encoding (default). */
31498
- UTF8: "utf8",
31499
- /** Use Latin-1 encoding. */
31500
- LATIN1: "latin1",
31501
- /** Use ASCII encoding. */
31502
- ASCII: "ascii"
31503
- };
31504
- var Dcm2xmlOptionsSchema = z.object({
31505
- timeoutMs: z.number().int().positive().optional(),
31506
- signal: z.instanceof(AbortSignal).optional(),
31507
- namespace: z.boolean().optional(),
31508
- charset: z.enum(["utf8", "latin1", "ascii"]).optional(),
31509
- writeBinaryData: z.boolean().optional(),
31510
- encodeBinaryBase64: z.boolean().optional()
31511
- }).strict().optional();
31512
- function buildArgs2(inputPath, options) {
31513
- const args = [];
31514
- if (options?.namespace === true) {
31515
- args.push("+Xn");
31516
- }
31517
- if (options?.charset === "latin1") {
31518
- args.push("+Cl");
31519
- } else if (options?.charset === "ascii") {
31520
- args.push("+Ca");
31521
- }
31522
- if (options?.writeBinaryData === true) {
31523
- args.push("+Wb");
31524
- if (options.encodeBinaryBase64 !== false) {
31525
- args.push("+Eb");
31526
- }
31527
- }
31528
- args.push(inputPath);
31529
- return args;
31530
- }
31531
- async function dcm2xml(inputPath, options) {
31532
- const validation = Dcm2xmlOptionsSchema.safeParse(options);
31533
- if (!validation.success) {
31534
- return err(new Error(`dcm2xml: invalid options: ${validation.error.message}`));
31535
- }
31536
- const binaryResult = resolveBinary("dcm2xml");
31537
- if (!binaryResult.ok) {
31538
- return err(binaryResult.error);
31539
- }
31540
- const args = buildArgs2(inputPath, options);
31541
- const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
31542
- const result = await execCommand(binaryResult.value, args, {
31543
- timeoutMs,
31544
- signal: options?.signal
31545
- });
31546
- if (!result.ok) {
31547
- return err(result.error);
31548
- }
31549
- if (result.value.exitCode !== 0) {
31550
- return err(createToolError("dcm2xml", args, result.value.exitCode, result.value.stderr));
31551
- }
31552
- return ok({ xml: result.value.stdout });
31553
- }
31554
31416
  var DcmdumpFormat = {
31555
31417
  /** Print standard DCMTK format. */
31556
31418
  STANDARD: "standard",
@@ -31565,7 +31427,7 @@ var DcmdumpOptionsSchema = z.object({
31565
31427
  searchTag: z.string().regex(/^\([0-9A-Fa-f]{4},[0-9A-Fa-f]{4}\)$/).optional(),
31566
31428
  printValues: z.boolean().optional()
31567
31429
  }).strict().optional();
31568
- function buildArgs3(inputPath, options) {
31430
+ function buildArgs2(inputPath, options) {
31569
31431
  const args = [];
31570
31432
  if (options?.format === "short") {
31571
31433
  args.push("+L");
@@ -31574,8 +31436,8 @@ function buildArgs3(inputPath, options) {
31574
31436
  args.push("+P", "all");
31575
31437
  }
31576
31438
  if (options?.searchTag !== void 0) {
31577
- const tag = options.searchTag.replace(/[()]/g, "");
31578
- args.push("+P", tag);
31439
+ const tag2 = options.searchTag.replace(/[()]/g, "");
31440
+ args.push("+P", tag2);
31579
31441
  }
31580
31442
  if (options?.printValues === true) {
31581
31443
  args.push("+Vr");
@@ -31586,13 +31448,13 @@ function buildArgs3(inputPath, options) {
31586
31448
  async function dcmdump(inputPath, options) {
31587
31449
  const validation = DcmdumpOptionsSchema.safeParse(options);
31588
31450
  if (!validation.success) {
31589
- return err(new Error(`dcmdump: invalid options: ${validation.error.message}`));
31451
+ return err(createValidationError("dcmdump", validation.error));
31590
31452
  }
31591
31453
  const binaryResult = resolveBinary("dcmdump");
31592
31454
  if (!binaryResult.ok) {
31593
31455
  return err(binaryResult.error);
31594
31456
  }
31595
- const args = buildArgs3(inputPath, options);
31457
+ const args = buildArgs2(inputPath, options);
31596
31458
  const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
31597
31459
  const result = await execCommand(binaryResult.value, args, {
31598
31460
  timeoutMs,
@@ -31631,7 +31493,7 @@ var DcmconvOptionsSchema = z.object({
31631
31493
  async function dcmconv(inputPath, outputPath, options) {
31632
31494
  const validation = DcmconvOptionsSchema.safeParse(options);
31633
31495
  if (!validation.success) {
31634
- return err(new Error(`dcmconv: invalid options: ${validation.error.message}`));
31496
+ return err(createValidationError("dcmconv", validation.error));
31635
31497
  }
31636
31498
  const binaryResult = resolveBinary("dcmconv");
31637
31499
  if (!binaryResult.ok) {
@@ -31651,6 +31513,70 @@ async function dcmconv(inputPath, outputPath, options) {
31651
31513
  }
31652
31514
  return ok({ outputPath });
31653
31515
  }
31516
+ 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+\])?)*)?$/;
31517
+ var TagModificationSchema = z.object({
31518
+ tag: z.string().regex(TAG_OR_PATH_PATTERN),
31519
+ value: z.string()
31520
+ });
31521
+ var DcmodifyOptionsSchema = z.object({
31522
+ timeoutMs: z.number().int().positive().optional(),
31523
+ signal: z.instanceof(AbortSignal).optional(),
31524
+ modifications: z.array(TagModificationSchema).optional().default([]),
31525
+ erasures: z.array(z.string()).optional(),
31526
+ erasePrivateTags: z.boolean().optional(),
31527
+ noBackup: z.boolean().optional(),
31528
+ insertIfMissing: z.boolean().optional(),
31529
+ ignoreMissingTags: z.boolean().optional()
31530
+ }).strict().refine((data) => data.modifications.length > 0 || data.erasures !== void 0 && data.erasures.length > 0 || data.erasePrivateTags === true, {
31531
+ message: "At least one of modifications, erasures, or erasePrivateTags is required"
31532
+ });
31533
+ function buildArgs3(inputPath, options) {
31534
+ const args = [];
31535
+ if (options.noBackup !== false) {
31536
+ args.push("-nb");
31537
+ }
31538
+ if (options.ignoreMissingTags === true) {
31539
+ args.push("-imt");
31540
+ }
31541
+ const flag = options.insertIfMissing === true ? "-i" : "-m";
31542
+ const modifications = options.modifications ?? [];
31543
+ for (const mod of modifications) {
31544
+ args.push(flag, `${mod.tag}=${mod.value}`);
31545
+ }
31546
+ if (options.erasures !== void 0) {
31547
+ for (const erasure of options.erasures) {
31548
+ args.push("-e", erasure);
31549
+ }
31550
+ }
31551
+ if (options.erasePrivateTags === true) {
31552
+ args.push("-ep");
31553
+ }
31554
+ args.push(inputPath);
31555
+ return args;
31556
+ }
31557
+ async function dcmodify(inputPath, options) {
31558
+ const validation = DcmodifyOptionsSchema.safeParse(options);
31559
+ if (!validation.success) {
31560
+ return err(createValidationError("dcmodify", validation.error));
31561
+ }
31562
+ const binaryResult = resolveBinary("dcmodify");
31563
+ if (!binaryResult.ok) {
31564
+ return err(binaryResult.error);
31565
+ }
31566
+ const args = buildArgs3(inputPath, options);
31567
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
31568
+ const result = await spawnCommand(binaryResult.value, args, {
31569
+ timeoutMs,
31570
+ signal: options.signal
31571
+ });
31572
+ if (!result.ok) {
31573
+ return err(result.error);
31574
+ }
31575
+ if (result.value.exitCode !== 0) {
31576
+ return err(createToolError("dcmodify", args, result.value.exitCode, result.value.stderr));
31577
+ }
31578
+ return ok({ filePath: inputPath });
31579
+ }
31654
31580
  var DcmftestOptionsSchema = z.object({
31655
31581
  timeoutMs: z.number().int().positive().optional(),
31656
31582
  signal: z.instanceof(AbortSignal).optional()
@@ -31658,7 +31584,7 @@ var DcmftestOptionsSchema = z.object({
31658
31584
  async function dcmftest(inputPath, options) {
31659
31585
  const validation = DcmftestOptionsSchema.safeParse(options);
31660
31586
  if (!validation.success) {
31661
- return err(new Error(`dcmftest: invalid options: ${validation.error.message}`));
31587
+ return err(createValidationError("dcmftest", validation.error));
31662
31588
  }
31663
31589
  const binaryResult = resolveBinary("dcmftest");
31664
31590
  if (!binaryResult.ok) {
@@ -31713,7 +31639,7 @@ function buildArgs4(options) {
31713
31639
  async function dcmgpdir(options) {
31714
31640
  const validation = DcmgpdirOptionsSchema.safeParse(options);
31715
31641
  if (!validation.success) {
31716
- return err(new Error(`dcmgpdir: invalid options: ${validation.error.message}`));
31642
+ return err(createValidationError("dcmgpdir", validation.error));
31717
31643
  }
31718
31644
  const binaryResult = resolveBinary("dcmgpdir");
31719
31645
  if (!binaryResult.ok) {
@@ -31772,7 +31698,7 @@ function buildArgs5(options) {
31772
31698
  async function dcmmkdir(options) {
31773
31699
  const validation = DcmmkdirOptionsSchema.safeParse(options);
31774
31700
  if (!validation.success) {
31775
- return err(new Error(`dcmmkdir: invalid options: ${validation.error.message}`));
31701
+ return err(createValidationError("dcmmkdir", validation.error));
31776
31702
  }
31777
31703
  const binaryResult = resolveBinary("dcmmkdir");
31778
31704
  if (!binaryResult.ok) {
@@ -31820,7 +31746,7 @@ function buildArgs6(options) {
31820
31746
  async function dcmqridx(options) {
31821
31747
  const validation = DcmqridxOptionsSchema.safeParse(options);
31822
31748
  if (!validation.success) {
31823
- return err(new Error(`dcmqridx: invalid options: ${validation.error.message}`));
31749
+ return err(createValidationError("dcmqridx", validation.error));
31824
31750
  }
31825
31751
  const binaryResult = resolveBinary("dcmqridx");
31826
31752
  if (!binaryResult.ok) {
@@ -31863,7 +31789,7 @@ function buildArgs7(inputPath, outputPath, options) {
31863
31789
  async function xml2dcm(inputPath, outputPath, options) {
31864
31790
  const validation = Xml2dcmOptionsSchema.safeParse(options);
31865
31791
  if (!validation.success) {
31866
- return err(new Error(`xml2dcm: invalid options: ${validation.error.message}`));
31792
+ return err(createValidationError("xml2dcm", validation.error));
31867
31793
  }
31868
31794
  const binaryResult = resolveBinary("xml2dcm");
31869
31795
  if (!binaryResult.ok) {
@@ -31890,7 +31816,7 @@ var Json2dcmOptionsSchema = z.object({
31890
31816
  async function json2dcm(inputPath, outputPath, options) {
31891
31817
  const validation = Json2dcmOptionsSchema.safeParse(options);
31892
31818
  if (!validation.success) {
31893
- return err(new Error(`json2dcm: invalid options: ${validation.error.message}`));
31819
+ return err(createValidationError("json2dcm", validation.error));
31894
31820
  }
31895
31821
  const binaryResult = resolveBinary("json2dcm");
31896
31822
  if (!binaryResult.ok) {
@@ -31930,7 +31856,7 @@ function buildArgs8(inputPath, outputPath, options) {
31930
31856
  async function dump2dcm(inputPath, outputPath, options) {
31931
31857
  const validation = Dump2dcmOptionsSchema.safeParse(options);
31932
31858
  if (!validation.success) {
31933
- return err(new Error(`dump2dcm: invalid options: ${validation.error.message}`));
31859
+ return err(createValidationError("dump2dcm", validation.error));
31934
31860
  }
31935
31861
  const binaryResult = resolveBinary("dump2dcm");
31936
31862
  if (!binaryResult.ok) {
@@ -31980,7 +31906,7 @@ function buildArgs9(inputPath, outputPath, options) {
31980
31906
  async function img2dcm(inputPath, outputPath, options) {
31981
31907
  const validation = Img2dcmOptionsSchema.safeParse(options);
31982
31908
  if (!validation.success) {
31983
- return err(new Error(`img2dcm: invalid options: ${validation.error.message}`));
31909
+ return err(createValidationError("img2dcm", validation.error));
31984
31910
  }
31985
31911
  const binaryResult = resolveBinary("img2dcm");
31986
31912
  if (!binaryResult.ok) {
@@ -32007,7 +31933,7 @@ var Pdf2dcmOptionsSchema = z.object({
32007
31933
  async function pdf2dcm(inputPath, outputPath, options) {
32008
31934
  const validation = Pdf2dcmOptionsSchema.safeParse(options);
32009
31935
  if (!validation.success) {
32010
- return err(new Error(`pdf2dcm: invalid options: ${validation.error.message}`));
31936
+ return err(createValidationError("pdf2dcm", validation.error));
32011
31937
  }
32012
31938
  const binaryResult = resolveBinary("pdf2dcm");
32013
31939
  if (!binaryResult.ok) {
@@ -32034,7 +31960,7 @@ var Dcm2pdfOptionsSchema = z.object({
32034
31960
  async function dcm2pdf(inputPath, outputPath, options) {
32035
31961
  const validation = Dcm2pdfOptionsSchema.safeParse(options);
32036
31962
  if (!validation.success) {
32037
- return err(new Error(`dcm2pdf: invalid options: ${validation.error.message}`));
31963
+ return err(createValidationError("dcm2pdf", validation.error));
32038
31964
  }
32039
31965
  const binaryResult = resolveBinary("dcm2pdf");
32040
31966
  if (!binaryResult.ok) {
@@ -32061,7 +31987,7 @@ var Cda2dcmOptionsSchema = z.object({
32061
31987
  async function cda2dcm(inputPath, outputPath, options) {
32062
31988
  const validation = Cda2dcmOptionsSchema.safeParse(options);
32063
31989
  if (!validation.success) {
32064
- return err(new Error(`cda2dcm: invalid options: ${validation.error.message}`));
31990
+ return err(createValidationError("cda2dcm", validation.error));
32065
31991
  }
32066
31992
  const binaryResult = resolveBinary("cda2dcm");
32067
31993
  if (!binaryResult.ok) {
@@ -32088,7 +32014,7 @@ var Dcm2cdaOptionsSchema = z.object({
32088
32014
  async function dcm2cda(inputPath, outputPath, options) {
32089
32015
  const validation = Dcm2cdaOptionsSchema.safeParse(options);
32090
32016
  if (!validation.success) {
32091
- return err(new Error(`dcm2cda: invalid options: ${validation.error.message}`));
32017
+ return err(createValidationError("dcm2cda", validation.error));
32092
32018
  }
32093
32019
  const binaryResult = resolveBinary("dcm2cda");
32094
32020
  if (!binaryResult.ok) {
@@ -32115,7 +32041,7 @@ var Stl2dcmOptionsSchema = z.object({
32115
32041
  async function stl2dcm(inputPath, outputPath, options) {
32116
32042
  const validation = Stl2dcmOptionsSchema.safeParse(options);
32117
32043
  if (!validation.success) {
32118
- return err(new Error(`stl2dcm: invalid options: ${validation.error.message}`));
32044
+ return err(createValidationError("stl2dcm", validation.error));
32119
32045
  }
32120
32046
  const binaryResult = resolveBinary("stl2dcm");
32121
32047
  if (!binaryResult.ok) {
@@ -32151,7 +32077,7 @@ function buildArgs10(inputPath, outputPath, options) {
32151
32077
  async function dcmcrle(inputPath, outputPath, options) {
32152
32078
  const validation = DcmcrleOptionsSchema.safeParse(options);
32153
32079
  if (!validation.success) {
32154
- return err(new Error(`dcmcrle: invalid options: ${validation.error.message}`));
32080
+ return err(createValidationError("dcmcrle", validation.error));
32155
32081
  }
32156
32082
  const binaryResult = resolveBinary("dcmcrle");
32157
32083
  if (!binaryResult.ok) {
@@ -32187,7 +32113,7 @@ function buildArgs11(inputPath, outputPath, options) {
32187
32113
  async function dcmdrle(inputPath, outputPath, options) {
32188
32114
  const validation = DcmdrleOptionsSchema.safeParse(options);
32189
32115
  if (!validation.success) {
32190
- return err(new Error(`dcmdrle: invalid options: ${validation.error.message}`));
32116
+ return err(createValidationError("dcmdrle", validation.error));
32191
32117
  }
32192
32118
  const binaryResult = resolveBinary("dcmdrle");
32193
32119
  if (!binaryResult.ok) {
@@ -32223,7 +32149,7 @@ function buildArgs12(inputPath, outputPath, options) {
32223
32149
  async function dcmencap(inputPath, outputPath, options) {
32224
32150
  const validation = DcmencapOptionsSchema.safeParse(options);
32225
32151
  if (!validation.success) {
32226
- return err(new Error(`dcmencap: invalid options: ${validation.error.message}`));
32152
+ return err(createValidationError("dcmencap", validation.error));
32227
32153
  }
32228
32154
  const binaryResult = resolveBinary("dcmencap");
32229
32155
  if (!binaryResult.ok) {
@@ -32250,7 +32176,7 @@ var DcmdecapOptionsSchema = z.object({
32250
32176
  async function dcmdecap(inputPath, outputPath, options) {
32251
32177
  const validation = DcmdecapOptionsSchema.safeParse(options);
32252
32178
  if (!validation.success) {
32253
- return err(new Error(`dcmdecap: invalid options: ${validation.error.message}`));
32179
+ return err(createValidationError("dcmdecap", validation.error));
32254
32180
  }
32255
32181
  const binaryResult = resolveBinary("dcmdecap");
32256
32182
  if (!binaryResult.ok) {
@@ -32290,7 +32216,7 @@ function buildArgs13(inputPath, outputPath, options) {
32290
32216
  async function dcmcjpeg(inputPath, outputPath, options) {
32291
32217
  const validation = DcmcjpegOptionsSchema.safeParse(options);
32292
32218
  if (!validation.success) {
32293
- return err(new Error(`dcmcjpeg: invalid options: ${validation.error.message}`));
32219
+ return err(createValidationError("dcmcjpeg", validation.error));
32294
32220
  }
32295
32221
  const binaryResult = resolveBinary("dcmcjpeg");
32296
32222
  if (!binaryResult.ok) {
@@ -32339,7 +32265,7 @@ function buildArgs14(inputPath, outputPath, options) {
32339
32265
  async function dcmdjpeg(inputPath, outputPath, options) {
32340
32266
  const validation = DcmdjpegOptionsSchema.safeParse(options);
32341
32267
  if (!validation.success) {
32342
- return err(new Error(`dcmdjpeg: invalid options: ${validation.error.message}`));
32268
+ return err(createValidationError("dcmdjpeg", validation.error));
32343
32269
  }
32344
32270
  const binaryResult = resolveBinary("dcmdjpeg");
32345
32271
  if (!binaryResult.ok) {
@@ -32381,7 +32307,7 @@ function buildArgs15(inputPath, outputPath, options) {
32381
32307
  async function dcmcjpls(inputPath, outputPath, options) {
32382
32308
  const validation = DcmcjplsOptionsSchema.safeParse(options);
32383
32309
  if (!validation.success) {
32384
- return err(new Error(`dcmcjpls: invalid options: ${validation.error.message}`));
32310
+ return err(createValidationError("dcmcjpls", validation.error));
32385
32311
  }
32386
32312
  const binaryResult = resolveBinary("dcmcjpls");
32387
32313
  if (!binaryResult.ok) {
@@ -32430,7 +32356,7 @@ function buildArgs16(inputPath, outputPath, options) {
32430
32356
  async function dcmdjpls(inputPath, outputPath, options) {
32431
32357
  const validation = DcmdjplsOptionsSchema.safeParse(options);
32432
32358
  if (!validation.success) {
32433
- return err(new Error(`dcmdjpls: invalid options: ${validation.error.message}`));
32359
+ return err(createValidationError("dcmdjpls", validation.error));
32434
32360
  }
32435
32361
  const binaryResult = resolveBinary("dcmdjpls");
32436
32362
  if (!binaryResult.ok) {
@@ -32489,7 +32415,7 @@ function buildArgs17(inputPath, outputPath, options) {
32489
32415
  async function dcmj2pnm(inputPath, outputPath, options) {
32490
32416
  const validation = Dcmj2pnmOptionsSchema.safeParse(options);
32491
32417
  if (!validation.success) {
32492
- return err(new Error(`dcmj2pnm: invalid options: ${validation.error.message}`));
32418
+ return err(createValidationError("dcmj2pnm", validation.error));
32493
32419
  }
32494
32420
  const binaryResult = resolveBinary("dcmj2pnm");
32495
32421
  if (!binaryResult.ok) {
@@ -32545,7 +32471,7 @@ function buildArgs18(inputPath, outputPath, options) {
32545
32471
  async function dcm2pnm(inputPath, outputPath, options) {
32546
32472
  const validation = Dcm2pnmOptionsSchema.safeParse(options);
32547
32473
  if (!validation.success) {
32548
- return err(new Error(`dcm2pnm: invalid options: ${validation.error.message}`));
32474
+ return err(createValidationError("dcm2pnm", validation.error));
32549
32475
  }
32550
32476
  const binaryResult = resolveBinary("dcm2pnm");
32551
32477
  if (!binaryResult.ok) {
@@ -32593,7 +32519,7 @@ function buildArgs19(inputPath, outputPath, options) {
32593
32519
  async function dcmscale(inputPath, outputPath, options) {
32594
32520
  const validation = DcmscaleOptionsSchema.safeParse(options);
32595
32521
  if (!validation.success) {
32596
- return err(new Error(`dcmscale: invalid options: ${validation.error.message}`));
32522
+ return err(createValidationError("dcmscale", validation.error));
32597
32523
  }
32598
32524
  const binaryResult = resolveBinary("dcmscale");
32599
32525
  if (!binaryResult.ok) {
@@ -32633,7 +32559,7 @@ function buildArgs20(inputPath, outputPath, options) {
32633
32559
  async function dcmquant(inputPath, outputPath, options) {
32634
32560
  const validation = DcmquantOptionsSchema.safeParse(options);
32635
32561
  if (!validation.success) {
32636
- return err(new Error(`dcmquant: invalid options: ${validation.error.message}`));
32562
+ return err(createValidationError("dcmquant", validation.error));
32637
32563
  }
32638
32564
  const binaryResult = resolveBinary("dcmquant");
32639
32565
  if (!binaryResult.ok) {
@@ -32680,7 +32606,7 @@ function buildArgs21(options) {
32680
32606
  async function dcmdspfn(options) {
32681
32607
  const validation = DcmdspfnOptionsSchema.safeParse(options);
32682
32608
  if (!validation.success) {
32683
- return err(new Error(`dcmdspfn: invalid options: ${validation.error.message}`));
32609
+ return err(createValidationError("dcmdspfn", validation.error));
32684
32610
  }
32685
32611
  const binaryResult = resolveBinary("dcmdspfn");
32686
32612
  if (!binaryResult.ok) {
@@ -32707,7 +32633,7 @@ var Dcod2lumOptionsSchema = z.object({
32707
32633
  async function dcod2lum(inputPath, outputPath, options) {
32708
32634
  const validation = Dcod2lumOptionsSchema.safeParse(options);
32709
32635
  if (!validation.success) {
32710
- return err(new Error(`dcod2lum: invalid options: ${validation.error.message}`));
32636
+ return err(createValidationError("dcod2lum", validation.error));
32711
32637
  }
32712
32638
  const binaryResult = resolveBinary("dcod2lum");
32713
32639
  if (!binaryResult.ok) {
@@ -32743,7 +32669,7 @@ function buildArgs22(inputPath, outputPath, options) {
32743
32669
  async function dconvlum(inputPath, outputPath, options) {
32744
32670
  const validation = DconvlumOptionsSchema.safeParse(options);
32745
32671
  if (!validation.success) {
32746
- return err(new Error(`dconvlum: invalid options: ${validation.error.message}`));
32672
+ return err(createValidationError("dconvlum", validation.error));
32747
32673
  }
32748
32674
  const binaryResult = resolveBinary("dconvlum");
32749
32675
  if (!binaryResult.ok) {
@@ -32833,7 +32759,7 @@ function buildArgs24(options) {
32833
32759
  async function dcmsend(options) {
32834
32760
  const validation = DcmsendOptionsSchema.safeParse(options);
32835
32761
  if (!validation.success) {
32836
- return err(new Error(`dcmsend: invalid options: ${validation.error.message}`));
32762
+ return err(createValidationError("dcmsend", validation.error));
32837
32763
  }
32838
32764
  const binaryResult = resolveBinary("dcmsend");
32839
32765
  if (!binaryResult.ok) {
@@ -32853,6 +32779,32 @@ async function dcmsend(options) {
32853
32779
  }
32854
32780
  return ok({ success: true, stderr: result.value.stderr });
32855
32781
  }
32782
+ var ProposedTransferSyntax = {
32783
+ UNCOMPRESSED: "uncompressed",
32784
+ LITTLE_ENDIAN: "littleEndian",
32785
+ BIG_ENDIAN: "bigEndian",
32786
+ IMPLICIT_VR: "implicitVR",
32787
+ JPEG_LOSSLESS: "jpegLossless",
32788
+ JPEG_8BIT: "jpeg8Bit",
32789
+ JPEG_12BIT: "jpeg12Bit",
32790
+ J2K_LOSSLESS: "j2kLossless",
32791
+ J2K_LOSSY: "j2kLossy",
32792
+ JLS_LOSSLESS: "jlsLossless",
32793
+ JLS_LOSSY: "jlsLossy"
32794
+ };
32795
+ var PROPOSED_TS_FLAG_MAP = {
32796
+ [ProposedTransferSyntax.UNCOMPRESSED]: "-x=",
32797
+ [ProposedTransferSyntax.LITTLE_ENDIAN]: "-xe",
32798
+ [ProposedTransferSyntax.BIG_ENDIAN]: "-xb",
32799
+ [ProposedTransferSyntax.IMPLICIT_VR]: "-xi",
32800
+ [ProposedTransferSyntax.JPEG_LOSSLESS]: "-xs",
32801
+ [ProposedTransferSyntax.JPEG_8BIT]: "-xy",
32802
+ [ProposedTransferSyntax.JPEG_12BIT]: "-xx",
32803
+ [ProposedTransferSyntax.J2K_LOSSLESS]: "-xv",
32804
+ [ProposedTransferSyntax.J2K_LOSSY]: "-xw",
32805
+ [ProposedTransferSyntax.JLS_LOSSLESS]: "-xt",
32806
+ [ProposedTransferSyntax.JLS_LOSSY]: "-xu"
32807
+ };
32856
32808
  var StorescuOptionsSchema = z.object({
32857
32809
  timeoutMs: z.number().int().positive().optional(),
32858
32810
  signal: z.instanceof(AbortSignal).optional(),
@@ -32862,7 +32814,20 @@ var StorescuOptionsSchema = z.object({
32862
32814
  callingAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
32863
32815
  calledAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
32864
32816
  scanDirectories: z.boolean().optional(),
32865
- recurse: z.boolean().optional()
32817
+ recurse: z.boolean().optional(),
32818
+ proposedTransferSyntax: z.enum([
32819
+ "uncompressed",
32820
+ "littleEndian",
32821
+ "bigEndian",
32822
+ "implicitVR",
32823
+ "jpegLossless",
32824
+ "jpeg8Bit",
32825
+ "jpeg12Bit",
32826
+ "j2kLossless",
32827
+ "j2kLossy",
32828
+ "jlsLossless",
32829
+ "jlsLossy"
32830
+ ]).optional()
32866
32831
  }).strict();
32867
32832
  function buildArgs25(options) {
32868
32833
  const args = [];
@@ -32878,6 +32843,9 @@ function buildArgs25(options) {
32878
32843
  if (options.recurse === true) {
32879
32844
  args.push("+r");
32880
32845
  }
32846
+ if (options.proposedTransferSyntax !== void 0) {
32847
+ args.push(PROPOSED_TS_FLAG_MAP[options.proposedTransferSyntax]);
32848
+ }
32881
32849
  args.push(options.host, String(options.port));
32882
32850
  args.push(...options.files);
32883
32851
  return args;
@@ -32885,7 +32853,7 @@ function buildArgs25(options) {
32885
32853
  async function storescu(options) {
32886
32854
  const validation = StorescuOptionsSchema.safeParse(options);
32887
32855
  if (!validation.success) {
32888
- return err(new Error(`storescu: invalid options: ${validation.error.message}`));
32856
+ return err(createValidationError("storescu", validation.error));
32889
32857
  }
32890
32858
  const binaryResult = resolveBinary("storescu");
32891
32859
  if (!binaryResult.ok) {
@@ -33025,7 +32993,7 @@ function buildArgs27(options) {
33025
32993
  async function movescu(options) {
33026
32994
  const validation = MovescuOptionsSchema.safeParse(options);
33027
32995
  if (!validation.success) {
33028
- return err(new Error(`movescu: invalid options: ${validation.error.message}`));
32996
+ return err(createValidationError("movescu", validation.error));
33029
32997
  }
33030
32998
  const binaryResult = resolveBinary("movescu");
33031
32999
  if (!binaryResult.ok) {
@@ -33089,7 +33057,7 @@ function buildArgs28(options) {
33089
33057
  async function getscu(options) {
33090
33058
  const validation = GetscuOptionsSchema.safeParse(options);
33091
33059
  if (!validation.success) {
33092
- return err(new Error(`getscu: invalid options: ${validation.error.message}`));
33060
+ return err(createValidationError("getscu", validation.error));
33093
33061
  }
33094
33062
  const binaryResult = resolveBinary("getscu");
33095
33063
  if (!binaryResult.ok) {
@@ -33131,7 +33099,7 @@ function buildArgs29(options) {
33131
33099
  async function termscu(options) {
33132
33100
  const validation = TermscuOptionsSchema.safeParse(options);
33133
33101
  if (!validation.success) {
33134
- return err(new Error(`termscu: invalid options: ${validation.error.message}`));
33102
+ return err(createValidationError("termscu", validation.error));
33135
33103
  }
33136
33104
  const binaryResult = resolveBinary("termscu");
33137
33105
  if (!binaryResult.ok) {
@@ -33175,7 +33143,7 @@ function buildArgs30(inputPath, options) {
33175
33143
  async function dsrdump(inputPath, options) {
33176
33144
  const validation = DsrdumpOptionsSchema.safeParse(options);
33177
33145
  if (!validation.success) {
33178
- return err(new Error(`dsrdump: invalid options: ${validation.error.message}`));
33146
+ return err(createValidationError("dsrdump", validation.error));
33179
33147
  }
33180
33148
  const binaryResult = resolveBinary("dsrdump");
33181
33149
  if (!binaryResult.ok) {
@@ -33215,7 +33183,7 @@ function buildArgs31(inputPath, options) {
33215
33183
  async function dsr2xml(inputPath, options) {
33216
33184
  const validation = Dsr2xmlOptionsSchema.safeParse(options);
33217
33185
  if (!validation.success) {
33218
- return err(new Error(`dsr2xml: invalid options: ${validation.error.message}`));
33186
+ return err(createValidationError("dsr2xml", validation.error));
33219
33187
  }
33220
33188
  const binaryResult = resolveBinary("dsr2xml");
33221
33189
  if (!binaryResult.ok) {
@@ -33255,7 +33223,7 @@ function buildArgs32(inputPath, outputPath, options) {
33255
33223
  async function xml2dsr(inputPath, outputPath, options) {
33256
33224
  const validation = Xml2dsrOptionsSchema.safeParse(options);
33257
33225
  if (!validation.success) {
33258
- return err(new Error(`xml2dsr: invalid options: ${validation.error.message}`));
33226
+ return err(createValidationError("xml2dsr", validation.error));
33259
33227
  }
33260
33228
  const binaryResult = resolveBinary("xml2dsr");
33261
33229
  if (!binaryResult.ok) {
@@ -33291,7 +33259,7 @@ function buildArgs33(inputPath, options) {
33291
33259
  async function drtdump(inputPath, options) {
33292
33260
  const validation = DrtdumpOptionsSchema.safeParse(options);
33293
33261
  if (!validation.success) {
33294
- return err(new Error(`drtdump: invalid options: ${validation.error.message}`));
33262
+ return err(createValidationError("drtdump", validation.error));
33295
33263
  }
33296
33264
  const binaryResult = resolveBinary("drtdump");
33297
33265
  if (!binaryResult.ok) {
@@ -33318,7 +33286,7 @@ var DcmpsmkOptionsSchema = z.object({
33318
33286
  async function dcmpsmk(inputPath, outputPath, options) {
33319
33287
  const validation = DcmpsmkOptionsSchema.safeParse(options);
33320
33288
  if (!validation.success) {
33321
- return err(new Error(`dcmpsmk: invalid options: ${validation.error.message}`));
33289
+ return err(createValidationError("dcmpsmk", validation.error));
33322
33290
  }
33323
33291
  const binaryResult = resolveBinary("dcmpsmk");
33324
33292
  if (!binaryResult.ok) {
@@ -33345,7 +33313,7 @@ var DcmpschkOptionsSchema = z.object({
33345
33313
  async function dcmpschk(inputPath, options) {
33346
33314
  const validation = DcmpschkOptionsSchema.safeParse(options);
33347
33315
  if (!validation.success) {
33348
- return err(new Error(`dcmpschk: invalid options: ${validation.error.message}`));
33316
+ return err(createValidationError("dcmpschk", validation.error));
33349
33317
  }
33350
33318
  const binaryResult = resolveBinary("dcmpschk");
33351
33319
  if (!binaryResult.ok) {
@@ -33391,7 +33359,7 @@ function buildArgs34(options) {
33391
33359
  async function dcmprscu(options) {
33392
33360
  const validation = DcmprscuOptionsSchema.safeParse(options);
33393
33361
  if (!validation.success) {
33394
- return err(new Error(`dcmprscu: invalid options: ${validation.error.message}`));
33362
+ return err(createValidationError("dcmprscu", validation.error));
33395
33363
  }
33396
33364
  const binaryResult = resolveBinary("dcmprscu");
33397
33365
  if (!binaryResult.ok) {
@@ -33427,7 +33395,7 @@ function buildArgs35(inputPath, options) {
33427
33395
  async function dcmpsprt(inputPath, options) {
33428
33396
  const validation = DcmpsprtOptionsSchema.safeParse(options);
33429
33397
  if (!validation.success) {
33430
- return err(new Error(`dcmpsprt: invalid options: ${validation.error.message}`));
33398
+ return err(createValidationError("dcmpsprt", validation.error));
33431
33399
  }
33432
33400
  const binaryResult = resolveBinary("dcmpsprt");
33433
33401
  if (!binaryResult.ok) {
@@ -33467,7 +33435,7 @@ function buildArgs36(inputPath, outputPath, options) {
33467
33435
  async function dcmp2pgm(inputPath, outputPath, options) {
33468
33436
  const validation = Dcmp2pgmOptionsSchema.safeParse(options);
33469
33437
  if (!validation.success) {
33470
- return err(new Error(`dcmp2pgm: invalid options: ${validation.error.message}`));
33438
+ return err(createValidationError("dcmp2pgm", validation.error));
33471
33439
  }
33472
33440
  const binaryResult = resolveBinary("dcmp2pgm");
33473
33441
  if (!binaryResult.ok) {
@@ -33494,7 +33462,7 @@ var DcmmkcrvOptionsSchema = z.object({
33494
33462
  async function dcmmkcrv(inputPath, outputPath, options) {
33495
33463
  const validation = DcmmkcrvOptionsSchema.safeParse(options);
33496
33464
  if (!validation.success) {
33497
- return err(new Error(`dcmmkcrv: invalid options: ${validation.error.message}`));
33465
+ return err(createValidationError("dcmmkcrv", validation.error));
33498
33466
  }
33499
33467
  const binaryResult = resolveBinary("dcmmkcrv");
33500
33468
  if (!binaryResult.ok) {
@@ -33555,7 +33523,7 @@ function buildArgs37(outputPath, options) {
33555
33523
  async function dcmmklut(outputPath, options) {
33556
33524
  const validation = DcmmklutOptionsSchema.safeParse(options);
33557
33525
  if (!validation.success) {
33558
- return err(new Error(`dcmmklut: invalid options: ${validation.error.message}`));
33526
+ return err(createValidationError("dcmmklut", validation.error));
33559
33527
  }
33560
33528
  const binaryResult = resolveBinary("dcmmklut");
33561
33529
  if (!binaryResult.ok) {
@@ -33575,6 +33543,363 @@ async function dcmmklut(outputPath, options) {
33575
33543
  }
33576
33544
  return ok({ outputPath });
33577
33545
  }
33546
+ async function applyModifications(filePath, changeset, options) {
33547
+ const modifications = changeset.toModifications();
33548
+ const erasures = changeset.toErasureArgs();
33549
+ const hasErasures = erasures.length > 0 || changeset.erasePrivate;
33550
+ const result = await dcmodify(filePath, {
33551
+ modifications: modifications.length > 0 ? modifications : void 0,
33552
+ erasures: erasures.length > 0 ? erasures : void 0,
33553
+ erasePrivateTags: changeset.erasePrivate || void 0,
33554
+ insertIfMissing: true,
33555
+ ignoreMissingTags: hasErasures || void 0,
33556
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
33557
+ signal: options.signal
33558
+ });
33559
+ if (!result.ok) return err(result.error);
33560
+ return ok(void 0);
33561
+ }
33562
+ async function copyFileSafe(source, dest) {
33563
+ return tryCatch(
33564
+ () => copyFile(source, dest),
33565
+ (e) => new Error(`Failed to copy file: ${e.message}`)
33566
+ );
33567
+ }
33568
+ async function statFileSize(path2) {
33569
+ return tryCatch(
33570
+ async () => (await stat(path2)).size,
33571
+ (e) => new Error(`Failed to stat file: ${e.message}`)
33572
+ );
33573
+ }
33574
+ async function unlinkFile(path2) {
33575
+ return tryCatch(
33576
+ () => unlink(path2),
33577
+ (e) => new Error(`Failed to delete file: ${e.message}`)
33578
+ );
33579
+ }
33580
+
33581
+ // src/dicom/DicomInstance.ts
33582
+ var DicomInstance = class _DicomInstance {
33583
+ constructor(dataset, changes, filePath, metadata) {
33584
+ __publicField(this, "dicomDataset");
33585
+ __publicField(this, "changeSet");
33586
+ __publicField(this, "filepath");
33587
+ __publicField(this, "meta");
33588
+ this.dicomDataset = dataset;
33589
+ this.changeSet = changes;
33590
+ this.filepath = filePath;
33591
+ this.meta = metadata;
33592
+ }
33593
+ // -----------------------------------------------------------------------
33594
+ // Factories
33595
+ // -----------------------------------------------------------------------
33596
+ /**
33597
+ * Opens a DICOM file and creates a DicomInstance.
33598
+ *
33599
+ * @param path - Filesystem path to the DICOM file
33600
+ * @param options - Timeout and abort options
33601
+ * @returns A Result containing the DicomInstance or an error
33602
+ */
33603
+ static async open(path2, options) {
33604
+ const filePathResult = createDicomFilePath(path2);
33605
+ if (!filePathResult.ok) return err(filePathResult.error);
33606
+ const jsonResult = await dcm2json(path2, {
33607
+ timeoutMs: options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
33608
+ signal: options?.signal
33609
+ });
33610
+ if (!jsonResult.ok) return err(jsonResult.error);
33611
+ const datasetResult = DicomDataset.fromJson(jsonResult.value.data);
33612
+ if (!datasetResult.ok) return err(datasetResult.error);
33613
+ return ok(new _DicomInstance(datasetResult.value, ChangeSet.empty(), filePathResult.value, /* @__PURE__ */ new Map()));
33614
+ }
33615
+ /**
33616
+ * Creates a DicomInstance from an existing DicomDataset.
33617
+ *
33618
+ * @param dataset - The DicomDataset to wrap
33619
+ * @param filePath - Optional file path (e.g. if the dataset came from a file)
33620
+ * @returns A Result containing the DicomInstance
33621
+ */
33622
+ static fromDataset(dataset, filePath) {
33623
+ let fp;
33624
+ if (filePath !== void 0) {
33625
+ const fpResult = createDicomFilePath(filePath);
33626
+ if (!fpResult.ok) return err(fpResult.error);
33627
+ fp = fpResult.value;
33628
+ }
33629
+ return ok(new _DicomInstance(dataset, ChangeSet.empty(), fp, /* @__PURE__ */ new Map()));
33630
+ }
33631
+ // -----------------------------------------------------------------------
33632
+ // Read accessors (delegate to DicomDataset)
33633
+ // -----------------------------------------------------------------------
33634
+ /** The underlying immutable DICOM dataset. */
33635
+ get dataset() {
33636
+ return this.dicomDataset;
33637
+ }
33638
+ /** The pending change set. */
33639
+ get changes() {
33640
+ return this.changeSet;
33641
+ }
33642
+ /** Whether there are unsaved changes. */
33643
+ get hasUnsavedChanges() {
33644
+ return !this.changeSet.isEmpty;
33645
+ }
33646
+ /** The file path, or undefined if this instance has no associated file. */
33647
+ get filePath() {
33648
+ return this.filepath;
33649
+ }
33650
+ /** Patient's Name (0010,0010). */
33651
+ get patientName() {
33652
+ return this.dicomDataset.patientName;
33653
+ }
33654
+ /** Patient ID (0010,0020). */
33655
+ get patientID() {
33656
+ return this.dicomDataset.patientID;
33657
+ }
33658
+ /** Study Date (0008,0020). */
33659
+ get studyDate() {
33660
+ return this.dicomDataset.studyDate;
33661
+ }
33662
+ /** Modality (0008,0060). */
33663
+ get modality() {
33664
+ return this.dicomDataset.modality;
33665
+ }
33666
+ /** Accession Number (0008,0050). */
33667
+ get accession() {
33668
+ return this.dicomDataset.accession;
33669
+ }
33670
+ /** SOP Class UID (0008,0016). */
33671
+ get sopClassUID() {
33672
+ return this.dicomDataset.sopClassUID;
33673
+ }
33674
+ /** Study Instance UID (0020,000D). */
33675
+ get studyInstanceUID() {
33676
+ return this.dicomDataset.studyInstanceUID;
33677
+ }
33678
+ /** Series Instance UID (0020,000E). */
33679
+ get seriesInstanceUID() {
33680
+ return this.dicomDataset.seriesInstanceUID;
33681
+ }
33682
+ /** SOP Instance UID (0008,0018). */
33683
+ get sopInstanceUID() {
33684
+ return this.dicomDataset.sopInstanceUID;
33685
+ }
33686
+ /** Transfer Syntax UID (0002,0010). */
33687
+ get transferSyntaxUID() {
33688
+ return this.dicomDataset.transferSyntaxUID;
33689
+ }
33690
+ /**
33691
+ * Gets a tag value as a string with optional fallback.
33692
+ *
33693
+ * @param tag - A DICOM tag, e.g. `'(0010,0010)'` or `'00100010'`
33694
+ * @param fallback - Value to return if tag is missing (default: `''`)
33695
+ */
33696
+ getString(tag2, fallback = "") {
33697
+ return this.dicomDataset.getString(tag2, fallback);
33698
+ }
33699
+ /**
33700
+ * Gets a tag value as a number.
33701
+ *
33702
+ * @param tag - A DICOM tag, e.g. `'(0020,0013)'`
33703
+ */
33704
+ getNumber(tag2) {
33705
+ return this.dicomDataset.getNumber(tag2);
33706
+ }
33707
+ /** Checks whether a tag exists in the dataset. */
33708
+ hasTag(tag2) {
33709
+ return this.dicomDataset.hasTag(tag2);
33710
+ }
33711
+ /**
33712
+ * Finds all values matching a wildcard path.
33713
+ *
33714
+ * @param path - A DicomTagPath with optional wildcard indices
33715
+ */
33716
+ findValues(path2) {
33717
+ return this.dicomDataset.findValues(path2);
33718
+ }
33719
+ // -----------------------------------------------------------------------
33720
+ // Write methods (return new instance)
33721
+ // -----------------------------------------------------------------------
33722
+ /**
33723
+ * Sets a tag value, returning a new DicomInstance.
33724
+ *
33725
+ * @param path - The DICOM tag path (e.g. `'(0010,0010)'`)
33726
+ * @param value - The new value
33727
+ */
33728
+ setTag(path2, value) {
33729
+ return new _DicomInstance(this.dicomDataset, this.changeSet.setTag(path2, value), this.filepath, this.meta);
33730
+ }
33731
+ /**
33732
+ * Erases a tag, returning a new DicomInstance.
33733
+ *
33734
+ * @param path - The DICOM tag path to erase
33735
+ */
33736
+ eraseTag(path2) {
33737
+ return new _DicomInstance(this.dicomDataset, this.changeSet.eraseTag(path2), this.filepath, this.meta);
33738
+ }
33739
+ /** Erases all private tags, returning a new DicomInstance. */
33740
+ erasePrivateTags() {
33741
+ return new _DicomInstance(this.dicomDataset, this.changeSet.erasePrivateTags(), this.filepath, this.meta);
33742
+ }
33743
+ /** Sets Patient's Name (0010,0010). */
33744
+ setPatientName(value) {
33745
+ return this.setTag("(0010,0010)", value);
33746
+ }
33747
+ /** Sets Patient ID (0010,0020). */
33748
+ setPatientID(value) {
33749
+ return this.setTag("(0010,0020)", value);
33750
+ }
33751
+ /** Sets Study Date (0008,0020). */
33752
+ setStudyDate(value) {
33753
+ return this.setTag("(0008,0020)", value);
33754
+ }
33755
+ /** Sets Modality (0008,0060). */
33756
+ setModality(value) {
33757
+ return this.setTag("(0008,0060)", value);
33758
+ }
33759
+ /** Sets Accession Number (0008,0050). */
33760
+ setAccessionNumber(value) {
33761
+ return this.setTag("(0008,0050)", value);
33762
+ }
33763
+ /** Sets Study Description (0008,1030). */
33764
+ setStudyDescription(value) {
33765
+ return this.setTag("(0008,1030)", value);
33766
+ }
33767
+ /** Sets Series Description (0008,103E). */
33768
+ setSeriesDescription(value) {
33769
+ return this.setTag("(0008,103E)", value);
33770
+ }
33771
+ /** Sets Institution Name (0008,0080). */
33772
+ setInstitutionName(value) {
33773
+ return this.setTag("(0008,0080)", value);
33774
+ }
33775
+ /**
33776
+ * Transforms a tag value using a function.
33777
+ *
33778
+ * The function receives the current string value (or undefined if tag is missing)
33779
+ * and returns the new value. Returns a new DicomInstance with the modification.
33780
+ *
33781
+ * @param path - The DICOM tag path
33782
+ * @param fn - Transform function receiving the current value
33783
+ */
33784
+ transformTag(path2, fn) {
33785
+ const current = this.dicomDataset.getString(path2);
33786
+ const newValue = fn(current.length > 0 ? current : void 0);
33787
+ return this.setTag(path2, newValue);
33788
+ }
33789
+ /**
33790
+ * Sets multiple tags at once, returning a new DicomInstance.
33791
+ *
33792
+ * @param entries - A record of tag path → value pairs
33793
+ */
33794
+ setBatch(entries) {
33795
+ return new _DicomInstance(this.dicomDataset, this.changeSet.setBatch(entries), this.filepath, this.meta);
33796
+ }
33797
+ /**
33798
+ * Returns a new DicomInstance with the given changes merged into pending changes.
33799
+ *
33800
+ * @param changes - A ChangeSet to merge with existing pending changes
33801
+ * @returns A new DicomInstance with accumulated changes
33802
+ */
33803
+ withChanges(changes) {
33804
+ return new _DicomInstance(this.dicomDataset, this.changeSet.merge(changes), this.filepath, this.meta);
33805
+ }
33806
+ /**
33807
+ * Returns a new DicomInstance pointing to a different file path.
33808
+ *
33809
+ * Preserves the dataset, pending changes, and metadata.
33810
+ *
33811
+ * @param newPath - The new filesystem path (validated via createDicomFilePath)
33812
+ * @returns A new DicomInstance with the updated path
33813
+ * @throws If the path is invalid
33814
+ */
33815
+ withFilePath(newPath) {
33816
+ const result = createDicomFilePath(newPath);
33817
+ if (!result.ok) throw result.error;
33818
+ return new _DicomInstance(this.dicomDataset, this.changeSet, result.value, this.meta);
33819
+ }
33820
+ // -----------------------------------------------------------------------
33821
+ // File I/O
33822
+ // -----------------------------------------------------------------------
33823
+ /**
33824
+ * Applies pending changes to the file in-place.
33825
+ *
33826
+ * Requires that the instance has an associated file path.
33827
+ *
33828
+ * @param options - Timeout and abort options
33829
+ */
33830
+ async applyChanges(options) {
33831
+ if (this.filepath === void 0) return err(new Error("No file path associated with this instance"));
33832
+ if (this.changeSet.isEmpty) return ok(void 0);
33833
+ return applyModifications(this.filepath, this.changeSet, options ?? {});
33834
+ }
33835
+ /**
33836
+ * Copies the file to a new path and applies pending changes to the copy.
33837
+ *
33838
+ * Returns a new DicomInstance pointing to the output path.
33839
+ *
33840
+ * @param outputPath - Destination filesystem path
33841
+ * @param options - Timeout and abort options
33842
+ */
33843
+ async writeAs(outputPath, options) {
33844
+ if (this.filepath === void 0) return err(new Error("No file path associated with this instance"));
33845
+ const outPathResult = createDicomFilePath(outputPath);
33846
+ if (!outPathResult.ok) return err(outPathResult.error);
33847
+ const copyResult = await copyFileSafe(this.filepath, outputPath);
33848
+ if (!copyResult.ok) return err(copyResult.error);
33849
+ if (!this.changeSet.isEmpty) {
33850
+ const applyResult = await applyModifications(outPathResult.value, this.changeSet, options ?? {});
33851
+ if (!applyResult.ok) {
33852
+ await unlinkFile(outputPath);
33853
+ return err(applyResult.error);
33854
+ }
33855
+ }
33856
+ return ok(new _DicomInstance(this.dicomDataset, ChangeSet.empty(), outPathResult.value, this.meta));
33857
+ }
33858
+ /**
33859
+ * Gets the file size in bytes.
33860
+ *
33861
+ * @returns A Result containing the size or an error
33862
+ */
33863
+ async fileSize() {
33864
+ if (this.filepath === void 0) return err(new Error("No file path associated with this instance"));
33865
+ return statFileSize(this.filepath);
33866
+ }
33867
+ /**
33868
+ * Deletes the associated file from the filesystem.
33869
+ *
33870
+ * @returns A Result indicating success or failure
33871
+ */
33872
+ async unlink() {
33873
+ if (this.filepath === void 0) return err(new Error("No file path associated with this instance"));
33874
+ return unlinkFile(this.filepath);
33875
+ }
33876
+ // -----------------------------------------------------------------------
33877
+ // Metadata (non-DICOM app context)
33878
+ // -----------------------------------------------------------------------
33879
+ /**
33880
+ * Returns a new DicomInstance with application metadata attached.
33881
+ *
33882
+ * Metadata is not stored in the DICOM file — it's for application context
33883
+ * (e.g. tracking source association, processing status, etc.).
33884
+ *
33885
+ * @param key - Metadata key
33886
+ * @param value - Metadata value
33887
+ */
33888
+ withMetadata(key, value) {
33889
+ const newMeta = new Map(this.meta);
33890
+ newMeta.set(key, value);
33891
+ return new _DicomInstance(this.dicomDataset, this.changeSet, this.filepath, newMeta);
33892
+ }
33893
+ /**
33894
+ * Gets application metadata by key.
33895
+ *
33896
+ * @param key - Metadata key
33897
+ * @returns The metadata value or undefined
33898
+ */
33899
+ getMetadata(key) {
33900
+ return this.meta.get(key);
33901
+ }
33902
+ };
33578
33903
 
33579
33904
  // src/events/dcmrecv.ts
33580
33905
  var DcmrecvEvent = {
@@ -33587,7 +33912,11 @@ var DcmrecvEvent = {
33587
33912
  ASSOCIATION_ABORTED: "ASSOCIATION_ABORTED",
33588
33913
  ECHO_REQUEST: "ECHO_REQUEST",
33589
33914
  CANNOT_START_LISTENER: "CANNOT_START_LISTENER",
33590
- REFUSING_ASSOCIATION: "REFUSING_ASSOCIATION"
33915
+ REFUSING_ASSOCIATION: "REFUSING_ASSOCIATION",
33916
+ /** Synthetic: STORED_FILE enriched with association context. */
33917
+ FILE_RECEIVED: "FILE_RECEIVED",
33918
+ /** Synthetic: emitted on association release/abort with summary. */
33919
+ ASSOCIATION_COMPLETE: "ASSOCIATION_COMPLETE"
33591
33920
  };
33592
33921
  var DCMRECV_PATTERNS = [
33593
33922
  {
@@ -33597,9 +33926,9 @@ var DCMRECV_PATTERNS = [
33597
33926
  },
33598
33927
  {
33599
33928
  event: DcmrecvEvent.ASSOCIATION_RECEIVED,
33600
- pattern: /Association Received\s{1,100}([^:]+):\s*"([^"]+)"\s*->\s*"([^"]+)"/,
33929
+ pattern: /Association Received\s{1,100}(\S+):\s+(\S+)\s+->\s+(\S+)/,
33601
33930
  processor: (match) => ({
33602
- address: match[1] ?? "",
33931
+ source: match[1] ?? "",
33603
33932
  callingAE: match[2] ?? "",
33604
33933
  calledAE: match[3] ?? ""
33605
33934
  })
@@ -33627,7 +33956,7 @@ var DCMRECV_PATTERNS = [
33627
33956
  },
33628
33957
  {
33629
33958
  event: DcmrecvEvent.ASSOCIATION_RELEASE,
33630
- pattern: /Received Association Release/i,
33959
+ pattern: /Association Release/i,
33631
33960
  processor: () => void 0
33632
33961
  },
33633
33962
  {
@@ -33663,6 +33992,11 @@ var StorescpEvent = {
33663
33992
  STORING_FILE: "STORING_FILE",
33664
33993
  SUBDIRECTORY_CREATED: "SUBDIRECTORY_CREATED"
33665
33994
  };
33995
+ var STORESCP_ASSOCIATION_RECEIVED = {
33996
+ event: StorescpEvent.ASSOCIATION_RECEIVED,
33997
+ pattern: /Association Received/i,
33998
+ processor: () => ({ source: "", callingAE: "", calledAE: "" })
33999
+ };
33666
34000
  var STORESCP_ADDITIONAL_PATTERNS = [
33667
34001
  {
33668
34002
  event: StorescpEvent.STORING_FILE,
@@ -33679,7 +34013,11 @@ var STORESCP_ADDITIONAL_PATTERNS = [
33679
34013
  })
33680
34014
  }
33681
34015
  ];
33682
- var STORESCP_PATTERNS = [...DCMRECV_PATTERNS, ...STORESCP_ADDITIONAL_PATTERNS];
34016
+ var STORESCP_PATTERNS = [
34017
+ ...DCMRECV_PATTERNS.filter((p) => p.event !== DcmrecvEvent.ASSOCIATION_RECEIVED),
34018
+ STORESCP_ASSOCIATION_RECEIVED,
34019
+ ...STORESCP_ADDITIONAL_PATTERNS
34020
+ ];
33683
34021
  var STORESCP_FATAL_EVENTS = /* @__PURE__ */ new Set([...DCMRECV_FATAL_EVENTS]);
33684
34022
 
33685
34023
  // src/events/dcmprscp.ts
@@ -33986,6 +34324,97 @@ var WLMSCPFS_PATTERNS = [
33986
34324
  }
33987
34325
  ];
33988
34326
  var WLMSCPFS_FATAL_EVENTS = /* @__PURE__ */ new Set([WlmscpfsEvent.CANNOT_START_LISTENER]);
34327
+
34328
+ // src/servers/AssociationTracker.ts
34329
+ var AssociationTracker = class {
34330
+ constructor() {
34331
+ __publicField(this, "association");
34332
+ __publicField(this, "counter", 0);
34333
+ }
34334
+ /**
34335
+ * Begins a new association, transitioning from IDLE to ACTIVE.
34336
+ *
34337
+ * If an association is already active, it is silently ended (abort)
34338
+ * and the new one begins.
34339
+ *
34340
+ * @param data - Association metadata
34341
+ * @returns The unique association ID
34342
+ */
34343
+ beginAssociation(data) {
34344
+ this.counter++;
34345
+ const associationId = `assoc-${String(this.counter)}`;
34346
+ this.association = {
34347
+ associationId,
34348
+ callingAE: data.callingAE,
34349
+ calledAE: data.calledAE,
34350
+ source: data.source,
34351
+ startTime: Date.now(),
34352
+ files: []
34353
+ };
34354
+ return associationId;
34355
+ }
34356
+ /**
34357
+ * Tracks a file received during the current association.
34358
+ *
34359
+ * If no association is active, returns a TrackedFile with empty context.
34360
+ *
34361
+ * @param filePath - Path to the received file
34362
+ * @returns A TrackedFile enriched with association context
34363
+ */
34364
+ trackFile(filePath) {
34365
+ if (this.association === void 0) {
34366
+ return {
34367
+ filePath,
34368
+ associationId: "",
34369
+ callingAE: "",
34370
+ calledAE: "",
34371
+ source: ""
34372
+ };
34373
+ }
34374
+ this.association.files.push(filePath);
34375
+ return {
34376
+ filePath,
34377
+ associationId: this.association.associationId,
34378
+ callingAE: this.association.callingAE,
34379
+ calledAE: this.association.calledAE,
34380
+ source: this.association.source
34381
+ };
34382
+ }
34383
+ /**
34384
+ * Ends the current association, transitioning from ACTIVE to IDLE.
34385
+ *
34386
+ * @param reason - Why the association ended
34387
+ * @returns An AssociationSummary, or undefined if no association was active
34388
+ */
34389
+ endAssociation(reason) {
34390
+ if (this.association === void 0) return void 0;
34391
+ const summary = {
34392
+ associationId: this.association.associationId,
34393
+ callingAE: this.association.callingAE,
34394
+ calledAE: this.association.calledAE,
34395
+ source: this.association.source,
34396
+ files: [...this.association.files],
34397
+ durationMs: Date.now() - this.association.startTime,
34398
+ endReason: reason
34399
+ };
34400
+ this.association = void 0;
34401
+ return summary;
34402
+ }
34403
+ /** The currently active association context, or undefined. */
34404
+ get current() {
34405
+ return this.association;
34406
+ }
34407
+ /** Whether an association is currently active. */
34408
+ get isActive() {
34409
+ return this.association !== void 0;
34410
+ }
34411
+ /** Resets the tracker to IDLE, discarding any active association. */
34412
+ reset() {
34413
+ this.association = void 0;
34414
+ }
34415
+ };
34416
+
34417
+ // src/servers/Dcmrecv.ts
33989
34418
  var SubdirectoryMode = {
33990
34419
  NONE: "none",
33991
34420
  SERIES_DATE: "series-date"
@@ -34072,10 +34501,13 @@ var Dcmrecv = class _Dcmrecv extends DcmtkProcess {
34072
34501
  constructor(config, parser2, signal) {
34073
34502
  super(config);
34074
34503
  __publicField(this, "parser");
34504
+ __publicField(this, "tracker");
34075
34505
  __publicField(this, "abortSignal");
34076
34506
  __publicField(this, "abortHandler");
34077
34507
  this.parser = parser2;
34508
+ this.tracker = new AssociationTracker();
34078
34509
  this.wireParser();
34510
+ this.wireTracker();
34079
34511
  if (signal !== void 0) {
34080
34512
  this.wireAbortSignal(signal);
34081
34513
  }
@@ -34116,6 +34548,24 @@ var Dcmrecv = class _Dcmrecv extends DcmtkProcess {
34116
34548
  onStoredFile(listener) {
34117
34549
  return this.onEvent("STORED_FILE", listener);
34118
34550
  }
34551
+ /**
34552
+ * Registers a listener for received files enriched with association context.
34553
+ *
34554
+ * @param listener - Callback receiving tracked file data
34555
+ * @returns this for chaining
34556
+ */
34557
+ onFileReceived(listener) {
34558
+ return this.onEvent("FILE_RECEIVED", listener);
34559
+ }
34560
+ /**
34561
+ * Registers a listener for completed associations.
34562
+ *
34563
+ * @param listener - Callback receiving association summary
34564
+ * @returns this for chaining
34565
+ */
34566
+ onAssociationComplete(listener) {
34567
+ return this.onEvent("ASSOCIATION_COMPLETE", listener);
34568
+ }
34119
34569
  /**
34120
34570
  * Creates a new Dcmrecv server instance.
34121
34571
  *
@@ -34125,7 +34575,7 @@ var Dcmrecv = class _Dcmrecv extends DcmtkProcess {
34125
34575
  static create(options) {
34126
34576
  const validation = DcmrecvOptionsSchema.safeParse(options);
34127
34577
  if (!validation.success) {
34128
- return err(new Error(`dcmrecv: invalid options: ${validation.error.message}`));
34578
+ return err(createValidationError("dcmrecv", validation.error));
34129
34579
  }
34130
34580
  const binaryResult = resolveBinary("dcmrecv");
34131
34581
  if (!binaryResult.ok) {
@@ -34158,7 +34608,29 @@ var Dcmrecv = class _Dcmrecv extends DcmtkProcess {
34158
34608
  this.emit("error", { error: new Error(`Fatal: ${event}`), fatal: true });
34159
34609
  void this.stop();
34160
34610
  }
34161
- this.emit(event, ...[data]);
34611
+ this.emit(event, data);
34612
+ });
34613
+ }
34614
+ /** Wires the AssociationTracker to server events. */
34615
+ wireTracker() {
34616
+ this.onEvent("ASSOCIATION_RECEIVED", (data) => {
34617
+ this.tracker.beginAssociation(data);
34618
+ });
34619
+ this.onEvent("STORED_FILE", (data) => {
34620
+ const tracked = this.tracker.trackFile(data.filePath);
34621
+ this.emit(DcmrecvEvent.FILE_RECEIVED, tracked);
34622
+ });
34623
+ this.onEvent("ASSOCIATION_RELEASE", () => {
34624
+ const summary = this.tracker.endAssociation("release");
34625
+ if (summary !== void 0) {
34626
+ this.emit(DcmrecvEvent.ASSOCIATION_COMPLETE, summary);
34627
+ }
34628
+ });
34629
+ this.onEvent("ASSOCIATION_ABORTED", () => {
34630
+ const summary = this.tracker.endAssociation("abort");
34631
+ if (summary !== void 0) {
34632
+ this.emit(DcmrecvEvent.ASSOCIATION_COMPLETE, summary);
34633
+ }
34162
34634
  });
34163
34635
  }
34164
34636
  /** Wires an AbortSignal to stop the server. */
@@ -34306,21 +34778,17 @@ var StoreSCP = class _StoreSCP extends DcmtkProcess {
34306
34778
  constructor(config, parser2, signal) {
34307
34779
  super(config);
34308
34780
  __publicField(this, "parser");
34781
+ __publicField(this, "tracker");
34309
34782
  __publicField(this, "abortSignal");
34310
34783
  __publicField(this, "abortHandler");
34311
34784
  this.parser = parser2;
34785
+ this.tracker = new AssociationTracker();
34312
34786
  this.wireParser();
34787
+ this.wireTracker();
34313
34788
  if (signal !== void 0) {
34314
34789
  this.wireAbortSignal(signal);
34315
34790
  }
34316
34791
  }
34317
- /**
34318
- * Registers a typed listener for a storescp-specific event.
34319
- *
34320
- * @param event - The event name from StoreSCPEventMap
34321
- * @param listener - Callback receiving typed event data
34322
- * @returns this for chaining
34323
- */
34324
34792
  /** Disposes the server and its parser, preventing listener leaks. */
34325
34793
  [Symbol.dispose]() {
34326
34794
  if (this.abortSignal !== void 0 && this.abortHandler !== void 0) {
@@ -34329,6 +34797,13 @@ var StoreSCP = class _StoreSCP extends DcmtkProcess {
34329
34797
  this.parser[Symbol.dispose]();
34330
34798
  super[Symbol.dispose]();
34331
34799
  }
34800
+ /**
34801
+ * Registers a typed listener for a storescp-specific event.
34802
+ *
34803
+ * @param event - The event name from StoreSCPEventMap
34804
+ * @param listener - Callback receiving typed event data
34805
+ * @returns this for chaining
34806
+ */
34332
34807
  onEvent(event, listener) {
34333
34808
  return this.on(event, listener);
34334
34809
  }
@@ -34350,6 +34825,24 @@ var StoreSCP = class _StoreSCP extends DcmtkProcess {
34350
34825
  onStoringFile(listener) {
34351
34826
  return this.onEvent("STORING_FILE", listener);
34352
34827
  }
34828
+ /**
34829
+ * Registers a listener for received files enriched with association context.
34830
+ *
34831
+ * @param listener - Callback receiving tracked file data
34832
+ * @returns this for chaining
34833
+ */
34834
+ onFileReceived(listener) {
34835
+ return this.onEvent("FILE_RECEIVED", listener);
34836
+ }
34837
+ /**
34838
+ * Registers a listener for completed associations.
34839
+ *
34840
+ * @param listener - Callback receiving association summary
34841
+ * @returns this for chaining
34842
+ */
34843
+ onAssociationComplete(listener) {
34844
+ return this.onEvent("ASSOCIATION_COMPLETE", listener);
34845
+ }
34353
34846
  /**
34354
34847
  * Creates a new StoreSCP server instance.
34355
34848
  *
@@ -34359,7 +34852,7 @@ var StoreSCP = class _StoreSCP extends DcmtkProcess {
34359
34852
  static create(options) {
34360
34853
  const validation = StoreSCPOptionsSchema.safeParse(options);
34361
34854
  if (!validation.success) {
34362
- return err(new Error(`storescp: invalid options: ${validation.error.message}`));
34855
+ return err(createValidationError("storescp", validation.error));
34363
34856
  }
34364
34857
  const binaryResult = resolveBinary("storescp");
34365
34858
  if (!binaryResult.ok) {
@@ -34392,7 +34885,33 @@ var StoreSCP = class _StoreSCP extends DcmtkProcess {
34392
34885
  this.emit("error", { error: new Error(`Fatal: ${event}`), fatal: true });
34393
34886
  void this.stop();
34394
34887
  }
34395
- this.emit(event, ...[data]);
34888
+ this.emit(event, data);
34889
+ });
34890
+ }
34891
+ /** Wires the AssociationTracker to server events. */
34892
+ wireTracker() {
34893
+ this.onEvent("ASSOCIATION_RECEIVED", (data) => {
34894
+ this.tracker.beginAssociation(data);
34895
+ });
34896
+ this.onEvent("STORING_FILE", (data) => {
34897
+ const tracked = this.tracker.trackFile(data.filePath);
34898
+ this.emit(DcmrecvEvent.FILE_RECEIVED, tracked);
34899
+ });
34900
+ this.onEvent("STORED_FILE", (data) => {
34901
+ const tracked = this.tracker.trackFile(data.filePath);
34902
+ this.emit(DcmrecvEvent.FILE_RECEIVED, tracked);
34903
+ });
34904
+ this.onEvent("ASSOCIATION_RELEASE", () => {
34905
+ const summary = this.tracker.endAssociation("release");
34906
+ if (summary !== void 0) {
34907
+ this.emit(DcmrecvEvent.ASSOCIATION_COMPLETE, summary);
34908
+ }
34909
+ });
34910
+ this.onEvent("ASSOCIATION_ABORTED", () => {
34911
+ const summary = this.tracker.endAssociation("abort");
34912
+ if (summary !== void 0) {
34913
+ this.emit(DcmrecvEvent.ASSOCIATION_COMPLETE, summary);
34914
+ }
34396
34915
  });
34397
34916
  }
34398
34917
  /** Wires an AbortSignal to stop the server. */
@@ -34491,7 +35010,7 @@ var DcmprsCP = class _DcmprsCP extends DcmtkProcess {
34491
35010
  static create(options) {
34492
35011
  const validation = DcmprsCPOptionsSchema.safeParse(options);
34493
35012
  if (!validation.success) {
34494
- return err(new Error(`dcmprscp: invalid options: ${validation.error.message}`));
35013
+ return err(createValidationError("dcmprscp", validation.error));
34495
35014
  }
34496
35015
  const binaryResult = resolveBinary("dcmprscp");
34497
35016
  if (!binaryResult.ok) {
@@ -34524,7 +35043,7 @@ var DcmprsCP = class _DcmprsCP extends DcmtkProcess {
34524
35043
  this.emit("error", { error: new Error(`Fatal: ${event}`), fatal: true });
34525
35044
  void this.stop();
34526
35045
  }
34527
- this.emit(event, ...[data]);
35046
+ this.emit(event, data);
34528
35047
  });
34529
35048
  }
34530
35049
  /** Wires an AbortSignal to stop the server. */
@@ -34620,7 +35139,7 @@ var Dcmpsrcv = class _Dcmpsrcv extends DcmtkProcess {
34620
35139
  static create(options) {
34621
35140
  const validation = DcmpsrcvOptionsSchema.safeParse(options);
34622
35141
  if (!validation.success) {
34623
- return err(new Error(`dcmpsrcv: invalid options: ${validation.error.message}`));
35142
+ return err(createValidationError("dcmpsrcv", validation.error));
34624
35143
  }
34625
35144
  const binaryResult = resolveBinary("dcmpsrcv");
34626
35145
  if (!binaryResult.ok) {
@@ -34653,7 +35172,7 @@ var Dcmpsrcv = class _Dcmpsrcv extends DcmtkProcess {
34653
35172
  this.emit("error", { error: new Error(`Fatal: ${event}`), fatal: true });
34654
35173
  void this.stop();
34655
35174
  }
34656
- this.emit(event, ...[data]);
35175
+ this.emit(event, data);
34657
35176
  });
34658
35177
  }
34659
35178
  /** Wires an AbortSignal to stop the server. */
@@ -34731,13 +35250,6 @@ var DcmQRSCP = class _DcmQRSCP extends DcmtkProcess {
34731
35250
  this.wireAbortSignal(signal);
34732
35251
  }
34733
35252
  }
34734
- /**
34735
- * Registers a typed listener for a dcmqrscp-specific event.
34736
- *
34737
- * @param event - The event name from DcmQRSCPEventMap
34738
- * @param listener - Callback receiving typed event data
34739
- * @returns this for chaining
34740
- */
34741
35253
  /** Disposes the server and its parser, preventing listener leaks. */
34742
35254
  [Symbol.dispose]() {
34743
35255
  if (this.abortSignal !== void 0 && this.abortHandler !== void 0) {
@@ -34746,6 +35258,13 @@ var DcmQRSCP = class _DcmQRSCP extends DcmtkProcess {
34746
35258
  this.parser[Symbol.dispose]();
34747
35259
  super[Symbol.dispose]();
34748
35260
  }
35261
+ /**
35262
+ * Registers a typed listener for a dcmqrscp-specific event.
35263
+ *
35264
+ * @param event - The event name from DcmQRSCPEventMap
35265
+ * @param listener - Callback receiving typed event data
35266
+ * @returns this for chaining
35267
+ */
34749
35268
  onEvent(event, listener) {
34750
35269
  return this.on(event, listener);
34751
35270
  }
@@ -34776,7 +35295,7 @@ var DcmQRSCP = class _DcmQRSCP extends DcmtkProcess {
34776
35295
  static create(options) {
34777
35296
  const validation = DcmQRSCPOptionsSchema.safeParse(options);
34778
35297
  if (!validation.success) {
34779
- return err(new Error(`dcmqrscp: invalid options: ${validation.error.message}`));
35298
+ return err(createValidationError("dcmqrscp", validation.error));
34780
35299
  }
34781
35300
  const binaryResult = resolveBinary("dcmqrscp");
34782
35301
  if (!binaryResult.ok) {
@@ -34808,7 +35327,7 @@ var DcmQRSCP = class _DcmQRSCP extends DcmtkProcess {
34808
35327
  this.emit("error", { error: new Error(`Fatal: ${event}`), fatal: true });
34809
35328
  void this.stop();
34810
35329
  }
34811
- this.emit(event, ...[data]);
35330
+ this.emit(event, data);
34812
35331
  });
34813
35332
  }
34814
35333
  /** Wires an AbortSignal to stop the server. */
@@ -34878,13 +35397,6 @@ var Wlmscpfs = class _Wlmscpfs extends DcmtkProcess {
34878
35397
  this.wireAbortSignal(signal);
34879
35398
  }
34880
35399
  }
34881
- /**
34882
- * Registers a typed listener for a wlmscpfs-specific event.
34883
- *
34884
- * @param event - The event name from WlmscpfsEventMap
34885
- * @param listener - Callback receiving typed event data
34886
- * @returns this for chaining
34887
- */
34888
35400
  /** Disposes the server and its parser, preventing listener leaks. */
34889
35401
  [Symbol.dispose]() {
34890
35402
  if (this.abortSignal !== void 0 && this.abortHandler !== void 0) {
@@ -34893,6 +35405,13 @@ var Wlmscpfs = class _Wlmscpfs extends DcmtkProcess {
34893
35405
  this.parser[Symbol.dispose]();
34894
35406
  super[Symbol.dispose]();
34895
35407
  }
35408
+ /**
35409
+ * Registers a typed listener for a wlmscpfs-specific event.
35410
+ *
35411
+ * @param event - The event name from WlmscpfsEventMap
35412
+ * @param listener - Callback receiving typed event data
35413
+ * @returns this for chaining
35414
+ */
34896
35415
  onEvent(event, listener) {
34897
35416
  return this.on(event, listener);
34898
35417
  }
@@ -34923,7 +35442,7 @@ var Wlmscpfs = class _Wlmscpfs extends DcmtkProcess {
34923
35442
  static create(options) {
34924
35443
  const validation = WlmscpfsOptionsSchema.safeParse(options);
34925
35444
  if (!validation.success) {
34926
- return err(new Error(`wlmscpfs: invalid options: ${validation.error.message}`));
35445
+ return err(createValidationError("wlmscpfs", validation.error));
34927
35446
  }
34928
35447
  const binaryResult = resolveBinary("wlmscpfs");
34929
35448
  if (!binaryResult.ok) {
@@ -34956,7 +35475,7 @@ var Wlmscpfs = class _Wlmscpfs extends DcmtkProcess {
34956
35475
  this.emit("error", { error: new Error(`Fatal: ${event}`), fatal: true });
34957
35476
  void this.stop();
34958
35477
  }
34959
- this.emit(event, ...[data]);
35478
+ this.emit(event, data);
34960
35479
  });
34961
35480
  }
34962
35481
  /** Wires an AbortSignal to stop the server. */
@@ -34972,6 +35491,506 @@ var Wlmscpfs = class _Wlmscpfs extends DcmtkProcess {
34972
35491
  signal.addEventListener("abort", this.abortHandler, { once: true });
34973
35492
  }
34974
35493
  };
35494
+ var DEFAULT_MIN_POOL_SIZE = 2;
35495
+ var DEFAULT_MAX_POOL_SIZE = 10;
35496
+ var DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
35497
+ var CONNECTION_RETRY_INTERVAL_MS = 500;
35498
+ var MAX_CONNECTION_RETRIES = 200;
35499
+ var DicomReceiverOptionsSchema = z.object({
35500
+ port: z.number().int().min(1).max(65535),
35501
+ storageDir: z.string().min(1).refine(isSafePath, { message: "path traversal detected in storageDir" }),
35502
+ aeTitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
35503
+ minPoolSize: z.number().int().min(1).max(100).optional(),
35504
+ maxPoolSize: z.number().int().min(1).max(100).optional(),
35505
+ connectionTimeoutMs: z.number().int().positive().optional(),
35506
+ configFile: z.string().min(1).refine(isSafePath, { message: "path traversal detected in configFile" }).optional(),
35507
+ configProfile: z.string().min(1).optional(),
35508
+ signal: z.instanceof(AbortSignal).optional()
35509
+ }).strict().refine((data) => (data.minPoolSize ?? DEFAULT_MIN_POOL_SIZE) <= (data.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE), {
35510
+ message: "minPoolSize must be <= maxPoolSize"
35511
+ });
35512
+ function allocatePort() {
35513
+ return new Promise((resolve) => {
35514
+ const server = net.createServer();
35515
+ server.listen(0, "127.0.0.1", () => {
35516
+ const addr = server.address();
35517
+ if (addr === null || typeof addr === "string") {
35518
+ server.close(() => resolve(err(new Error("Failed to allocate port"))));
35519
+ return;
35520
+ }
35521
+ const port = addr.port;
35522
+ server.close(() => resolve(ok(port)));
35523
+ });
35524
+ server.on("error", (e) => {
35525
+ resolve(err(new Error(`Port allocation failed: ${e.message}`)));
35526
+ });
35527
+ });
35528
+ }
35529
+ var DicomReceiver = class _DicomReceiver extends EventEmitter {
35530
+ constructor(options) {
35531
+ super();
35532
+ __publicField(this, "options");
35533
+ __publicField(this, "minPoolSize");
35534
+ __publicField(this, "maxPoolSize");
35535
+ __publicField(this, "connectionTimeoutMs");
35536
+ __publicField(this, "workers", /* @__PURE__ */ new Map());
35537
+ __publicField(this, "tcpServer");
35538
+ __publicField(this, "associationCounter", 0);
35539
+ __publicField(this, "started", false);
35540
+ __publicField(this, "stopping", false);
35541
+ __publicField(this, "abortHandler");
35542
+ this.setMaxListeners(20);
35543
+ this.on("error", () => {
35544
+ });
35545
+ this.options = options;
35546
+ this.minPoolSize = options.minPoolSize ?? DEFAULT_MIN_POOL_SIZE;
35547
+ this.maxPoolSize = options.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE;
35548
+ this.connectionTimeoutMs = options.connectionTimeoutMs ?? DEFAULT_CONNECTION_TIMEOUT_MS;
35549
+ }
35550
+ // -----------------------------------------------------------------------
35551
+ // Public API
35552
+ // -----------------------------------------------------------------------
35553
+ /**
35554
+ * Creates a new DicomReceiver instance.
35555
+ *
35556
+ * @param options - Configuration options
35557
+ * @returns A Result containing the instance or a validation error
35558
+ */
35559
+ static create(options) {
35560
+ const validation = DicomReceiverOptionsSchema.safeParse(options);
35561
+ if (!validation.success) {
35562
+ return err(createValidationError("DicomReceiver", validation.error));
35563
+ }
35564
+ return ok(new _DicomReceiver(options));
35565
+ }
35566
+ /**
35567
+ * Starts the TCP proxy and spawns the initial worker pool.
35568
+ *
35569
+ * @returns A Result indicating success or failure
35570
+ */
35571
+ async start() {
35572
+ if (this.started) {
35573
+ return err(new Error("DicomReceiver: already started"));
35574
+ }
35575
+ this.started = true;
35576
+ const storageDirResult = await ensureDirectory(this.options.storageDir);
35577
+ if (!storageDirResult.ok) return storageDirResult;
35578
+ const spawnResults = await this.spawnWorkers(this.minPoolSize);
35579
+ if (!spawnResults.ok) return spawnResults;
35580
+ const listenResult = await this.startTcpProxy();
35581
+ if (!listenResult.ok) return listenResult;
35582
+ if (this.options.signal !== void 0) {
35583
+ this.wireAbortSignal(this.options.signal);
35584
+ }
35585
+ return ok(void 0);
35586
+ }
35587
+ /**
35588
+ * Stops the TCP proxy and all workers.
35589
+ */
35590
+ async stop() {
35591
+ if (!this.started || this.stopping) {
35592
+ return;
35593
+ }
35594
+ this.stopping = true;
35595
+ if (this.options.signal !== void 0 && this.abortHandler !== void 0) {
35596
+ this.options.signal.removeEventListener("abort", this.abortHandler);
35597
+ }
35598
+ await this.closeTcpProxy();
35599
+ const stopPromises = [];
35600
+ for (const worker of this.workers.values()) {
35601
+ stopPromises.push(this.stopWorker(worker));
35602
+ }
35603
+ await Promise.all(stopPromises);
35604
+ this.workers.clear();
35605
+ this.started = false;
35606
+ this.stopping = false;
35607
+ }
35608
+ /**
35609
+ * Registers a typed listener for a DicomReceiver-specific event.
35610
+ *
35611
+ * @param event - The event name from DicomReceiverEventMap
35612
+ * @param listener - Callback receiving typed event data
35613
+ * @returns this for chaining
35614
+ */
35615
+ onEvent(event, listener) {
35616
+ return this.on(event, listener);
35617
+ }
35618
+ /**
35619
+ * Registers a listener for received files.
35620
+ *
35621
+ * @param listener - Callback receiving file data
35622
+ * @returns this for chaining
35623
+ */
35624
+ onFileReceived(listener) {
35625
+ return this.on("FILE_RECEIVED", listener);
35626
+ }
35627
+ /**
35628
+ * Registers a listener for completed associations.
35629
+ *
35630
+ * @param listener - Callback receiving association data
35631
+ * @returns this for chaining
35632
+ */
35633
+ onAssociationComplete(listener) {
35634
+ return this.on("ASSOCIATION_COMPLETE", listener);
35635
+ }
35636
+ /** Current pool status. */
35637
+ get poolStatus() {
35638
+ let idle = 0;
35639
+ let busy = 0;
35640
+ for (const w of this.workers.values()) {
35641
+ if (w.state === "idle") idle++;
35642
+ else busy++;
35643
+ }
35644
+ return { idle, busy, total: this.workers.size };
35645
+ }
35646
+ // -----------------------------------------------------------------------
35647
+ // TCP proxy
35648
+ // -----------------------------------------------------------------------
35649
+ /** Starts the TCP proxy on the configured port. */
35650
+ startTcpProxy() {
35651
+ return new Promise((resolve) => {
35652
+ this.tcpServer = net.createServer((socket) => {
35653
+ void this.handleConnection(socket);
35654
+ });
35655
+ this.tcpServer.on("error", (e) => {
35656
+ if (!this.started) {
35657
+ resolve(err(new Error(`DicomReceiver: TCP proxy failed: ${e.message}`)));
35658
+ } else {
35659
+ this.emit("error", { error: e instanceof Error ? e : new Error(String(e)) });
35660
+ }
35661
+ });
35662
+ this.tcpServer.listen(this.options.port, () => {
35663
+ resolve(ok(void 0));
35664
+ });
35665
+ });
35666
+ }
35667
+ /** Closes the TCP proxy server. */
35668
+ closeTcpProxy() {
35669
+ return new Promise((resolve) => {
35670
+ if (this.tcpServer === void 0) {
35671
+ resolve();
35672
+ return;
35673
+ }
35674
+ this.tcpServer.close(() => resolve());
35675
+ });
35676
+ }
35677
+ // -----------------------------------------------------------------------
35678
+ // Connection routing
35679
+ // -----------------------------------------------------------------------
35680
+ /** Routes an incoming connection to an idle worker. */
35681
+ async handleConnection(remoteSocket) {
35682
+ remoteSocket.pause();
35683
+ const worker = await this.findIdleWorker();
35684
+ if (worker === void 0) {
35685
+ remoteSocket.destroy(new Error("DicomReceiver: no idle worker available"));
35686
+ this.emit("error", { error: new Error("DicomReceiver: connection rejected \u2014 pool exhausted") });
35687
+ return;
35688
+ }
35689
+ this.associationCounter++;
35690
+ const associationId = `assoc-${String(this.associationCounter)}`;
35691
+ const associationDir = path.join(this.options.storageDir, associationId);
35692
+ const mkdirResult = await ensureDirectory(associationDir);
35693
+ if (!mkdirResult.ok) {
35694
+ remoteSocket.destroy();
35695
+ this.emit("error", { error: mkdirResult.error });
35696
+ return;
35697
+ }
35698
+ worker.state = "busy";
35699
+ worker.associationId = associationId;
35700
+ worker.associationDir = associationDir;
35701
+ worker.files = [];
35702
+ worker.fileSizes = [];
35703
+ worker.startAt = Date.now();
35704
+ this.pipeConnection(worker, remoteSocket);
35705
+ void this.replenishPool();
35706
+ }
35707
+ /** Finds an idle worker, retrying up to connectionTimeoutMs. */
35708
+ async findIdleWorker() {
35709
+ const idle = this.getIdleWorker();
35710
+ if (idle !== void 0) return idle;
35711
+ const maxRetries = Math.min(Math.ceil(this.connectionTimeoutMs / CONNECTION_RETRY_INTERVAL_MS), MAX_CONNECTION_RETRIES);
35712
+ for (let i = 0; i < maxRetries; i++) {
35713
+ await delay(CONNECTION_RETRY_INTERVAL_MS);
35714
+ const found = this.getIdleWorker();
35715
+ if (found !== void 0) return found;
35716
+ if (this.stopping) return void 0;
35717
+ }
35718
+ return void 0;
35719
+ }
35720
+ /** Returns the first idle worker, or undefined. */
35721
+ getIdleWorker() {
35722
+ for (const w of this.workers.values()) {
35723
+ if (w.state === "idle") return w;
35724
+ }
35725
+ return void 0;
35726
+ }
35727
+ /** Pipes remote socket bidirectionally to the worker's port. */
35728
+ pipeConnection(worker, remoteSocket) {
35729
+ const workerSocket = net.createConnection({ port: worker.port, host: "127.0.0.1" });
35730
+ worker.remoteSocket = remoteSocket;
35731
+ worker.workerSocket = workerSocket;
35732
+ remoteSocket.pipe(workerSocket);
35733
+ workerSocket.pipe(remoteSocket);
35734
+ remoteSocket.resume();
35735
+ const cleanup = () => {
35736
+ remoteSocket.unpipe(workerSocket);
35737
+ workerSocket.unpipe(remoteSocket);
35738
+ if (!remoteSocket.destroyed) remoteSocket.destroy();
35739
+ if (!workerSocket.destroyed) workerSocket.destroy();
35740
+ };
35741
+ remoteSocket.on("error", cleanup);
35742
+ workerSocket.on("error", cleanup);
35743
+ remoteSocket.on("close", cleanup);
35744
+ workerSocket.on("close", cleanup);
35745
+ }
35746
+ // -----------------------------------------------------------------------
35747
+ // Worker pool management
35748
+ // -----------------------------------------------------------------------
35749
+ /** Spawns `count` new workers and adds them to the pool. */
35750
+ async spawnWorkers(count) {
35751
+ const promises = [];
35752
+ for (let i = 0; i < count; i++) {
35753
+ promises.push(this.spawnWorker());
35754
+ }
35755
+ const results = await Promise.all(promises);
35756
+ for (const result of results) {
35757
+ if (!result.ok) return err(result.error);
35758
+ }
35759
+ return ok(void 0);
35760
+ }
35761
+ /** Spawns a single Dcmrecv worker with an ephemeral port. */
35762
+ async spawnWorker() {
35763
+ const portResult = await allocatePort();
35764
+ if (!portResult.ok) return portResult;
35765
+ const port = portResult.value;
35766
+ const tempDir = path.join(os.tmpdir(), `dcmrecv-pool-${String(port)}-${String(Date.now())}`);
35767
+ const mkdirResult = await ensureDirectory(tempDir);
35768
+ if (!mkdirResult.ok) return mkdirResult;
35769
+ const createResult = Dcmrecv.create({
35770
+ port,
35771
+ aeTitle: this.options.aeTitle ?? "DCMRECV",
35772
+ outputDirectory: tempDir,
35773
+ configFile: this.options.configFile,
35774
+ configProfile: this.options.configProfile
35775
+ });
35776
+ if (!createResult.ok) return createResult;
35777
+ const dcmrecv = createResult.value;
35778
+ const worker = {
35779
+ dcmrecv,
35780
+ port,
35781
+ tempDir,
35782
+ state: "idle",
35783
+ associationId: void 0,
35784
+ associationDir: void 0,
35785
+ files: [],
35786
+ fileSizes: [],
35787
+ startAt: void 0,
35788
+ remoteSocket: void 0,
35789
+ workerSocket: void 0
35790
+ };
35791
+ this.wireWorkerEvents(worker);
35792
+ const startResult = await dcmrecv.start();
35793
+ if (!startResult.ok) {
35794
+ return err(new Error(`DicomReceiver: worker start failed on port ${String(port)}: ${startResult.error.message}`));
35795
+ }
35796
+ this.workers.set(port, worker);
35797
+ return ok(worker);
35798
+ }
35799
+ /** Stops a single worker: stop process, clean temp dir, remove from pool. */
35800
+ async stopWorker(worker) {
35801
+ if (worker.remoteSocket !== void 0 && !worker.remoteSocket.destroyed) {
35802
+ worker.remoteSocket.destroy();
35803
+ }
35804
+ if (worker.workerSocket !== void 0 && !worker.workerSocket.destroyed) {
35805
+ worker.workerSocket.destroy();
35806
+ }
35807
+ await worker.dcmrecv.stop();
35808
+ worker.dcmrecv[Symbol.dispose]();
35809
+ await removeDirSafe(worker.tempDir);
35810
+ this.workers.delete(worker.port);
35811
+ }
35812
+ /** Pre-emptively spawns workers to keep idle count >= minPoolSize. */
35813
+ async replenishPool() {
35814
+ const status = this.poolStatus;
35815
+ const needed = this.minPoolSize - status.idle;
35816
+ const capacity = this.maxPoolSize - status.total;
35817
+ const toSpawn = Math.min(needed, capacity);
35818
+ if (toSpawn <= 0) return;
35819
+ const promises = [];
35820
+ for (let i = 0; i < toSpawn; i++) {
35821
+ promises.push(this.spawnWorker());
35822
+ }
35823
+ const results = await Promise.all(promises);
35824
+ for (const result of results) {
35825
+ if (!result.ok) {
35826
+ this.emit("error", { error: result.error });
35827
+ }
35828
+ }
35829
+ }
35830
+ /** Stops excess idle workers when idle count > minPoolSize + 2. */
35831
+ async scaleDown() {
35832
+ const idleWorkers = [];
35833
+ for (const w of this.workers.values()) {
35834
+ if (w.state === "idle") idleWorkers.push(w);
35835
+ }
35836
+ const excess = idleWorkers.length - (this.minPoolSize + 2);
35837
+ if (excess <= 0) return;
35838
+ const toStop = idleWorkers.slice(0, excess);
35839
+ const promises = [];
35840
+ for (const w of toStop) {
35841
+ promises.push(this.stopWorker(w));
35842
+ }
35843
+ await Promise.all(promises);
35844
+ }
35845
+ // -----------------------------------------------------------------------
35846
+ // Worker event wiring
35847
+ // -----------------------------------------------------------------------
35848
+ /** Wires FILE_RECEIVED and ASSOCIATION_COMPLETE events on a worker. */
35849
+ wireWorkerEvents(worker) {
35850
+ this.wireFileReceived(worker);
35851
+ this.wireAssociationComplete(worker);
35852
+ }
35853
+ /** Wires FILE_RECEIVED from dcmrecv worker to handleFileReceived. */
35854
+ wireFileReceived(worker) {
35855
+ worker.dcmrecv.onFileReceived((data) => {
35856
+ void this.handleFileReceived(worker, data);
35857
+ });
35858
+ }
35859
+ /** Moves a received file, opens it as DicomInstance, and emits FILE_RECEIVED. */
35860
+ async handleFileReceived(worker, data) {
35861
+ if (worker.associationDir === void 0 || worker.associationId === void 0) return;
35862
+ const srcPath = data.filePath;
35863
+ const destPath = path.join(worker.associationDir, path.basename(srcPath));
35864
+ const assocId = worker.associationId;
35865
+ const assocDir = worker.associationDir;
35866
+ const moveResult = await moveFile(srcPath, destPath);
35867
+ const finalPath = moveResult.ok ? destPath : srcPath;
35868
+ worker.files.push(finalPath);
35869
+ worker.fileSizes.push(await statFileSafe(finalPath));
35870
+ const openResult = await DicomInstance.open(finalPath);
35871
+ if (!openResult.ok) {
35872
+ this.emit("error", {
35873
+ error: openResult.error,
35874
+ filePath: finalPath,
35875
+ associationId: assocId,
35876
+ associationDir: assocDir,
35877
+ callingAE: data.callingAE,
35878
+ calledAE: data.calledAE,
35879
+ source: data.source
35880
+ });
35881
+ return;
35882
+ }
35883
+ this.emit("FILE_RECEIVED", {
35884
+ filePath: finalPath,
35885
+ associationId: assocId,
35886
+ associationDir: assocDir,
35887
+ callingAE: data.callingAE,
35888
+ calledAE: data.calledAE,
35889
+ source: data.source,
35890
+ instance: openResult.value
35891
+ });
35892
+ }
35893
+ /** Returns worker to idle pool on association complete, emits summary. */
35894
+ wireAssociationComplete(worker) {
35895
+ worker.dcmrecv.onAssociationComplete((data) => {
35896
+ const assocId = worker.associationId ?? data.associationId;
35897
+ const assocDir = worker.associationDir ?? "";
35898
+ const files = [...worker.files];
35899
+ const endAt = Date.now();
35900
+ const startAt = worker.startAt ?? endAt;
35901
+ const totalBytes = sumArray(worker.fileSizes);
35902
+ const elapsedMs = endAt - startAt;
35903
+ const bytesPerSecond = elapsedMs > 0 ? Math.round(totalBytes / elapsedMs * 1e3) : 0;
35904
+ this.emit("ASSOCIATION_COMPLETE", {
35905
+ associationId: assocId,
35906
+ associationDir: assocDir,
35907
+ callingAE: data.callingAE,
35908
+ calledAE: data.calledAE,
35909
+ source: data.source,
35910
+ files,
35911
+ durationMs: data.durationMs,
35912
+ endReason: data.endReason,
35913
+ totalBytes,
35914
+ bytesPerSecond,
35915
+ startAt,
35916
+ endAt
35917
+ });
35918
+ worker.state = "idle";
35919
+ worker.associationId = void 0;
35920
+ worker.associationDir = void 0;
35921
+ worker.files = [];
35922
+ worker.fileSizes = [];
35923
+ worker.startAt = void 0;
35924
+ worker.remoteSocket = void 0;
35925
+ worker.workerSocket = void 0;
35926
+ void this.scaleDown();
35927
+ });
35928
+ }
35929
+ // -----------------------------------------------------------------------
35930
+ // Abort signal
35931
+ // -----------------------------------------------------------------------
35932
+ /** Wires an AbortSignal to stop the receiver. */
35933
+ wireAbortSignal(signal) {
35934
+ if (signal.aborted) {
35935
+ void this.stop();
35936
+ return;
35937
+ }
35938
+ this.abortHandler = () => {
35939
+ void this.stop();
35940
+ };
35941
+ signal.addEventListener("abort", this.abortHandler, { once: true });
35942
+ }
35943
+ };
35944
+ async function ensureDirectory(dirPath) {
35945
+ try {
35946
+ await fs.mkdir(dirPath, { recursive: true });
35947
+ return ok(void 0);
35948
+ } catch (e) {
35949
+ const msg = e instanceof Error ? e.message : String(e);
35950
+ return err(new Error(`Failed to create directory ${dirPath}: ${msg}`));
35951
+ }
35952
+ }
35953
+ async function moveFile(src, dest) {
35954
+ try {
35955
+ await fs.rename(src, dest);
35956
+ return ok(void 0);
35957
+ } catch {
35958
+ try {
35959
+ await fs.copyFile(src, dest);
35960
+ await fs.unlink(src);
35961
+ return ok(void 0);
35962
+ } catch (e) {
35963
+ const msg = e instanceof Error ? e.message : String(e);
35964
+ return err(new Error(`Failed to move file ${src} \u2192 ${dest}: ${msg}`));
35965
+ }
35966
+ }
35967
+ }
35968
+ async function statFileSafe(filePath) {
35969
+ try {
35970
+ const stat3 = await fs.stat(filePath);
35971
+ return stat3.size;
35972
+ } catch {
35973
+ return 0;
35974
+ }
35975
+ }
35976
+ function sumArray(arr) {
35977
+ let total = 0;
35978
+ for (let i = 0; i < arr.length; i++) {
35979
+ total += arr[i] ?? 0;
35980
+ }
35981
+ return total;
35982
+ }
35983
+ async function removeDirSafe(dirPath) {
35984
+ try {
35985
+ await fs.rm(dirPath, { recursive: true, force: true });
35986
+ } catch {
35987
+ }
35988
+ }
35989
+ function delay(ms) {
35990
+ return new Promise((resolve) => {
35991
+ setTimeout(resolve, ms);
35992
+ });
35993
+ }
34975
35994
 
34976
35995
  // src/pacs/types.ts
34977
35996
  var QueryLevel = {
@@ -35054,11 +36073,11 @@ function addFilterKeys(keys, filter) {
35054
36073
  for (let i = 0; i < fields.length; i += 1) {
35055
36074
  const field = fields[i];
35056
36075
  if (field === void 0) continue;
35057
- const tag = FILTER_TAG_MAP[field];
35058
- if (tag === void 0) continue;
36076
+ const tag2 = FILTER_TAG_MAP[field];
36077
+ if (tag2 === void 0) continue;
35059
36078
  const value = filter[field];
35060
36079
  if (typeof value !== "string") continue;
35061
- keys.push(`${tag}=${value}`);
36080
+ keys.push(`${tag2}=${value}`);
35062
36081
  }
35063
36082
  }
35064
36083
  function extractTag(key) {
@@ -35067,11 +36086,11 @@ function extractTag(key) {
35067
36086
  }
35068
36087
  function addReturnKeys(keys, returnKeys) {
35069
36088
  for (let i = 0; i < returnKeys.length; i += 1) {
35070
- const tag = returnKeys[i];
35071
- if (tag === void 0) continue;
35072
- const alreadyPresent = keys.some((k) => extractTag(k) === tag);
36089
+ const tag2 = returnKeys[i];
36090
+ if (tag2 === void 0) continue;
36091
+ const alreadyPresent = keys.some((k) => extractTag(k) === tag2);
35073
36092
  if (!alreadyPresent) {
35074
- keys.push(`${tag}=`);
36093
+ keys.push(`${tag2}=`);
35075
36094
  }
35076
36095
  }
35077
36096
  }
@@ -35581,6 +36600,6 @@ async function retry(operation, options) {
35581
36600
  return lastResult;
35582
36601
  }
35583
36602
 
35584
- export { AETitleSchema, ChangeSet, ColorConversion, DCMPRSCP_FATAL_EVENTS, DCMPRSCP_PATTERNS, DCMPSRCV_FATAL_EVENTS, DCMPSRCV_PATTERNS, DCMQRSCP_FATAL_EVENTS, DCMQRSCP_PATTERNS, DCMRECV_FATAL_EVENTS, DCMRECV_PATTERNS, DEFAULT_BLOCK_TIMEOUT_MS, DEFAULT_DICOM_PORT, DEFAULT_DRAIN_TIMEOUT_MS, DEFAULT_PARSE_CONCURRENCY, DEFAULT_START_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, Dcm2pnmOutputFormat, Dcm2xmlCharset, DcmQRSCP, DcmdumpFormat, Dcmj2pnmOutputFormat, DcmprsCP, DcmprscpEvent, Dcmpsrcv, DcmpsrcvEvent, DcmqrscpEvent, Dcmrecv, DcmrecvEvent, DcmtkProcess, DicomDataset, DicomFile, DicomTagPathSchema, DicomTagSchema, FilenameMode, GetQueryModel, Img2dcmInputFormat, JplsColorConversion, LineParser, LutType, MAX_BLOCK_LINES, MAX_CHANGESET_OPERATIONS, MAX_EVENT_PATTERNS, MAX_TRAVERSAL_DEPTH, MoveQueryModel, PDU_SIZE, PacsClient, PortSchema, PreferredTransferSyntax, ProcessState, QueryLevel, QueryModel, REQUIRED_BINARIES, RetrieveMode, SOP_CLASSES, STORESCP_FATAL_EVENTS, STORESCP_PATTERNS, StorageMode, StoreSCP, StoreSCPPreset, StorescpEvent, SubdirectoryMode, TransferSyntax, UIDSchema, UNIX_SEARCH_PATHS, VR, VR_CATEGORY, VR_CATEGORY_NAME, VR_META, WINDOWS_SEARCH_PATHS, WLMSCPFS_FATAL_EVENTS, WLMSCPFS_PATTERNS, Wlmscpfs, WlmscpfsEvent, assertUnreachable, batch, cda2dcm, clearDcmtkPathCache, createAETitle, createDicomFilePath, createDicomTag, createDicomTagPath, createPort, createSOPClassUID, createTransferSyntaxUID, dcm2cda, dcm2json, dcm2pdf, dcm2pnm, dcm2xml, dcmcjpeg, dcmcjpls, dcmconv, dcmcrle, dcmdecap, dcmdjpeg, dcmdjpls, dcmdrle, dcmdspfn, dcmdump, dcmencap, dcmftest, dcmgpdir, dcmj2pnm, dcmmkcrv, dcmmkdir, dcmmklut, dcmodify, dcmp2pgm, dcmprscu, dcmpschk, dcmpsmk, dcmpsprt, dcmqridx, dcmquant, dcmscale, dcmsend, dcod2lum, dconvlum, drtdump, dsr2xml, dsrdump, dump2dcm, echoscu, err, execCommand, findDcmtkPath, findscu, getVRCategory, getscu, img2dcm, isBinaryVR, isNumericVR, isStringVR, json2dcm, lookupTag, lookupTagByKeyword, lookupTagByName, mapResult, movescu, ok, parseAETitle, parseDicomTag, parseDicomTagPath, parsePort, parseSOPClassUID, parseTransferSyntaxUID, pdf2dcm, retry, segmentsToModifyPath, segmentsToString, sopClassNameFromUID, spawnCommand, stl2dcm, storescu, tagPathToSegments, termscu, unwrap, xml2dcm, xml2dsr, xmlToJson };
36603
+ export { AETitleSchema, AssociationTracker, ChangeSet, ColorConversion, DCMPRSCP_FATAL_EVENTS, DCMPRSCP_PATTERNS, DCMPSRCV_FATAL_EVENTS, DCMPSRCV_PATTERNS, DCMQRSCP_FATAL_EVENTS, DCMQRSCP_PATTERNS, DCMRECV_FATAL_EVENTS, DCMRECV_PATTERNS, DEFAULT_BLOCK_TIMEOUT_MS, DEFAULT_DICOM_PORT, DEFAULT_DRAIN_TIMEOUT_MS, DEFAULT_PARSE_CONCURRENCY, DEFAULT_START_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, Dcm2pnmOutputFormat, Dcm2xmlCharset, DcmQRSCP, DcmdumpFormat, Dcmj2pnmOutputFormat, DcmprsCP, DcmprscpEvent, Dcmpsrcv, DcmpsrcvEvent, DcmqrscpEvent, Dcmrecv, DcmrecvEvent, DcmtkProcess, DicomDataset, DicomInstance, DicomReceiver, DicomTagPathSchema, DicomTagSchema, FilenameMode, GetQueryModel, Img2dcmInputFormat, JplsColorConversion, LineParser, LutType, MAX_BLOCK_LINES, MAX_CHANGESET_OPERATIONS, MAX_EVENT_PATTERNS, MAX_TRAVERSAL_DEPTH, MoveQueryModel, PDU_SIZE, PacsClient, PortSchema, PreferredTransferSyntax, ProcessState, ProposedTransferSyntax, QueryLevel, QueryModel, REQUIRED_BINARIES, RetrieveMode, SOP_CLASSES, STORESCP_FATAL_EVENTS, STORESCP_PATTERNS, StorageMode, StoreSCP, StoreSCPPreset, StorescpEvent, SubdirectoryMode, TransferSyntax, UIDSchema, UNIX_SEARCH_PATHS, VR, VR_CATEGORY, VR_CATEGORY_NAME, VR_META, WINDOWS_SEARCH_PATHS, WLMSCPFS_FATAL_EVENTS, WLMSCPFS_PATTERNS, Wlmscpfs, WlmscpfsEvent, assertUnreachable, batch, cda2dcm, clearDcmtkPathCache, createAETitle, createDicomFilePath, createDicomTag, createDicomTagPath, createPort, createSOPClassUID, createTransferSyntaxUID, dcm2cda, dcm2json, dcm2pdf, dcm2pnm, dcm2xml, dcmcjpeg, dcmcjpls, dcmconv, dcmcrle, dcmdecap, dcmdjpeg, dcmdjpls, dcmdrle, dcmdspfn, dcmdump, dcmencap, dcmftest, dcmgpdir, dcmj2pnm, dcmmkcrv, dcmmkdir, dcmmklut, dcmodify, dcmp2pgm, dcmprscu, dcmpschk, dcmpsmk, dcmpsprt, dcmqridx, dcmquant, dcmscale, dcmsend, dcod2lum, dconvlum, drtdump, dsr2xml, dsrdump, dump2dcm, echoscu, err, execCommand, findDcmtkPath, findscu, getVRCategory, getscu, img2dcm, isBinaryVR, isNumericVR, isStringVR, json2dcm, lookupTag, lookupTagByKeyword, lookupTagByName, mapResult, movescu, ok, parseAETitle, parseDicomTag, parseDicomTagPath, parsePort, parseSOPClassUID, parseTransferSyntaxUID, pdf2dcm, retry, segmentsToModifyPath, segmentsToString, sopClassNameFromUID, spawnCommand, stl2dcm, storescu, tag, tagPathToSegments, termscu, xml2dcm, xml2dsr, xmlToJson };
35585
36604
  //# sourceMappingURL=index.js.map
35586
36605
  //# sourceMappingURL=index.js.map