@ubercode/dcmtk 0.1.4 → 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.
- package/README.md +18 -15
- package/dist/DicomInstance-By9zd7GM.d.cts +625 -0
- package/dist/DicomInstance-CQEIuF_x.d.ts +625 -0
- package/dist/{dcmodify-CTXBWKU9.d.cts → dcmodify-B-_uUIKB.d.ts} +4 -2
- package/dist/{dcmodify-Daeafqrm.d.ts → dcmodify-Gds9u5Vj.d.cts} +4 -2
- package/dist/dicom.cjs +329 -51
- package/dist/dicom.cjs.map +1 -1
- package/dist/dicom.d.cts +368 -3
- package/dist/dicom.d.ts +368 -3
- package/dist/dicom.js +329 -51
- package/dist/dicom.js.map +1 -1
- package/dist/index.cjs +1460 -419
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -7
- package/dist/index.d.ts +8 -7
- package/dist/index.js +1432 -413
- package/dist/index.js.map +1 -1
- package/dist/servers.cjs +2379 -196
- package/dist/servers.cjs.map +1 -1
- package/dist/servers.d.cts +1654 -3
- package/dist/servers.d.ts +1654 -3
- package/dist/servers.js +2305 -145
- package/dist/servers.js.map +1 -1
- package/dist/tools.cjs +97 -50
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +21 -4
- package/dist/tools.d.ts +21 -4
- package/dist/tools.js +97 -51
- package/dist/tools.js.map +1 -1
- package/dist/{types-zHhxS7d2.d.cts → types-Cgumy1N4.d.cts} +1 -24
- package/dist/{types-zHhxS7d2.d.ts → types-Cgumy1N4.d.ts} +1 -24
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js.map +1 -1
- package/package.json +8 -8
- package/dist/index-BZxi4104.d.ts +0 -826
- package/dist/index-CapkWqxy.d.ts +0 -1295
- package/dist/index-DX4C3zbo.d.cts +0 -826
- 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(
|
|
30257
|
-
const key =
|
|
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(
|
|
30427
|
+
function tagPathToSegments(path2) {
|
|
30425
30428
|
const segments = [];
|
|
30426
|
-
let remaining =
|
|
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 ${
|
|
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(
|
|
30472
|
-
const parenMatch = TAG_WITH_PARENS.exec(
|
|
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: "${
|
|
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(
|
|
30480
|
-
return ok(
|
|
30482
|
+
if (TAG_HEX_ONLY.test(tag2)) {
|
|
30483
|
+
return ok(tag2.toUpperCase());
|
|
30481
30484
|
}
|
|
30482
|
-
return err(new Error(`Invalid tag format: "${
|
|
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(
|
|
30664
|
-
const norm = normalizeTag(
|
|
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 ${
|
|
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(
|
|
30677
|
-
const elemResult = this.getElement(
|
|
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 ${
|
|
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(
|
|
30690
|
-
const valResult = this.getValue(
|
|
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 ${
|
|
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(
|
|
30707
|
-
const elemResult = this.getElement(
|
|
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(
|
|
30719
|
-
const elemResult = this.getElement(
|
|
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(
|
|
30732
|
-
const elemResult = this.getElement(
|
|
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(
|
|
30743
|
-
const norm = normalizeTag(
|
|
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(
|
|
30756
|
+
getElementAtPath(path2) {
|
|
30754
30757
|
let segments;
|
|
30755
30758
|
try {
|
|
30756
|
-
segments = tagPathToSegments(
|
|
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(
|
|
30776
|
+
findValues(path2) {
|
|
30774
30777
|
let segments;
|
|
30775
30778
|
try {
|
|
30776
|
-
segments = tagPathToSegments(
|
|
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(
|
|
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(
|
|
30901
|
+
tagPathToSegments(path2);
|
|
30887
30902
|
const sanitized = sanitizeValue(value);
|
|
30888
30903
|
const newMods = new Map(this.mods);
|
|
30889
|
-
newMods.set(
|
|
30904
|
+
newMods.set(path2, sanitized);
|
|
30890
30905
|
const newErasures = new Set(this.erased);
|
|
30891
|
-
newErasures.delete(
|
|
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(
|
|
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(
|
|
30923
|
+
tagPathToSegments(path2);
|
|
30909
30924
|
const newMods = new Map(this.mods);
|
|
30910
|
-
newMods.delete(
|
|
30925
|
+
newMods.delete(path2);
|
|
30911
30926
|
const newErasures = new Set(this.erased);
|
|
30912
|
-
newErasures.add(
|
|
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 [
|
|
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
|
|
30988
|
-
if (
|
|
30989
|
-
result.push(
|
|
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
|
|
31032
|
-
parts.push(`${
|
|
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
|
|
31173
|
-
if (
|
|
31174
|
-
result[
|
|
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
|
|
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
|
|
31578
|
-
args.push("+P",
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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}(
|
|
33929
|
+
pattern: /Association Received\s{1,100}(\S+):\s+(\S+)\s+->\s+(\S+)/,
|
|
33601
33930
|
processor: (match) => ({
|
|
33602
|
-
|
|
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: /
|
|
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 = [
|
|
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(
|
|
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,
|
|
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(
|
|
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,
|
|
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(
|
|
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,
|
|
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(
|
|
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,
|
|
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(
|
|
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,
|
|
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(
|
|
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,
|
|
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
|
|
35058
|
-
if (
|
|
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(`${
|
|
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
|
|
35071
|
-
if (
|
|
35072
|
-
const alreadyPresent = keys.some((k) => extractTag(k) ===
|
|
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(`${
|
|
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,
|
|
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
|