@ubercode/dcmtk 0.4.0 → 0.6.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/dist/{DicomInstance-CGBr3a-C.d.ts → DicomInstance-BjrSIEGN.d.ts} +50 -2
- package/dist/{DicomInstance-DWOjhccQ.d.cts → DicomInstance-zsmyd7fs.d.cts} +50 -2
- package/dist/{dcmodify-BvaIeyJg.d.ts → dcmodify-CHvwChFu.d.ts} +1 -1
- package/dist/{dcmodify-B9js5K1f.d.cts → dcmodify-Cf-RPHF3.d.cts} +1 -1
- package/dist/dicom.cjs +72 -7
- package/dist/dicom.cjs.map +1 -1
- package/dist/dicom.d.cts +3 -3
- package/dist/dicom.d.ts +3 -3
- package/dist/dicom.js +72 -7
- package/dist/dicom.js.map +1 -1
- package/dist/index-BuGhfFlQ.d.cts +1735 -0
- package/dist/index-DylL4aB-.d.ts +1735 -0
- package/dist/index.cjs +203 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +203 -19
- package/dist/index.js.map +1 -1
- package/dist/servers.cjs +202 -18
- package/dist/servers.cjs.map +1 -1
- package/dist/servers.d.cts +6 -1654
- package/dist/servers.d.ts +6 -1654
- package/dist/servers.js +202 -18
- package/dist/servers.js.map +1 -1
- package/dist/tools.cjs +24 -7
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +2 -2
- package/dist/tools.d.ts +2 -2
- package/dist/tools.js +24 -7
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
package/dist/servers.cjs
CHANGED
|
@@ -1557,6 +1557,45 @@ var ChangeSet = class _ChangeSet {
|
|
|
1557
1557
|
return result;
|
|
1558
1558
|
}
|
|
1559
1559
|
};
|
|
1560
|
+
|
|
1561
|
+
// src/dicom/walkTags.ts
|
|
1562
|
+
var DEFAULT_MAX_DEPTH = 16;
|
|
1563
|
+
function walkTags(data, options) {
|
|
1564
|
+
const ctx = {
|
|
1565
|
+
maxDepth: options?.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
1566
|
+
vrSet: options?.vrFilter !== void 0 ? new Set(options.vrFilter) : void 0,
|
|
1567
|
+
results: []
|
|
1568
|
+
};
|
|
1569
|
+
walkLevel(data, 0, "", ctx);
|
|
1570
|
+
return ctx.results;
|
|
1571
|
+
}
|
|
1572
|
+
function walkLevel(data, depth, pathPrefix, ctx) {
|
|
1573
|
+
const keys = Object.keys(data);
|
|
1574
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1575
|
+
const tag = keys[i];
|
|
1576
|
+
if (tag === void 0) continue;
|
|
1577
|
+
const element = data[tag];
|
|
1578
|
+
if (element === void 0) continue;
|
|
1579
|
+
const vr = element.vr;
|
|
1580
|
+
const path2 = pathPrefix.length > 0 ? `${pathPrefix}.${tag}` : tag;
|
|
1581
|
+
if (ctx.vrSet === void 0 || ctx.vrSet.has(vr)) {
|
|
1582
|
+
ctx.results.push({ tag, element, vr, depth, path: path2 });
|
|
1583
|
+
}
|
|
1584
|
+
if (vr === "SQ" && element.Value !== void 0 && depth < ctx.maxDepth) {
|
|
1585
|
+
walkSequenceItems(element.Value, depth, path2, ctx);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
function walkSequenceItems(items, depth, parentPath, ctx) {
|
|
1590
|
+
for (let idx = 0; idx < items.length; idx++) {
|
|
1591
|
+
const item = items[idx];
|
|
1592
|
+
if (typeof item !== "object" || item === null) continue;
|
|
1593
|
+
const itemPath = `${parentPath}[${idx}]`;
|
|
1594
|
+
walkLevel(item, depth + 1, itemPath, ctx);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// src/dicom/DicomDataset.ts
|
|
1560
1599
|
var HEX_TAG_KEY = /^[0-9A-Fa-f]{8}$/;
|
|
1561
1600
|
function isDicomJsonElement(value) {
|
|
1562
1601
|
return typeof value === "object" && value !== null && "vr" in value && typeof value["vr"] === "string";
|
|
@@ -1885,6 +1924,15 @@ var DicomDataset = class _DicomDataset {
|
|
|
1885
1924
|
const result = collectWildcard(this.data, segments);
|
|
1886
1925
|
return result.values;
|
|
1887
1926
|
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Walks all tags in this dataset, optionally filtering by VR and recursing into sequences.
|
|
1929
|
+
*
|
|
1930
|
+
* @param options - Optional VR filter and max depth
|
|
1931
|
+
* @returns A readonly array of all matching tag entries
|
|
1932
|
+
*/
|
|
1933
|
+
walkTags(options) {
|
|
1934
|
+
return walkTags(this.data, options);
|
|
1935
|
+
}
|
|
1888
1936
|
// -----------------------------------------------------------------------
|
|
1889
1937
|
// Convenience readonly getters
|
|
1890
1938
|
// -----------------------------------------------------------------------
|
|
@@ -2097,15 +2145,32 @@ function convertSequence(attr, element) {
|
|
|
2097
2145
|
}
|
|
2098
2146
|
if (values.length > 0) element.Value = values;
|
|
2099
2147
|
}
|
|
2100
|
-
|
|
2148
|
+
var NUMERIC_JSON_VRS = /* @__PURE__ */ new Set(["DS", "FL", "FD", "IS", "SL", "SS", "SV", "UL", "US", "UV"]);
|
|
2149
|
+
function unwrapValue(v) {
|
|
2150
|
+
if (typeof v !== "object" || v === null) return v;
|
|
2151
|
+
const obj = v;
|
|
2152
|
+
if ("#text" in obj) return obj["#text"];
|
|
2153
|
+
const keys = Object.keys(obj);
|
|
2154
|
+
if (keys.length === 1 && keys[0] !== void 0 && keys[0].startsWith("@_")) {
|
|
2155
|
+
return obj[keys[0]];
|
|
2156
|
+
}
|
|
2157
|
+
return v;
|
|
2158
|
+
}
|
|
2159
|
+
function coerceNumeric(value) {
|
|
2160
|
+
if (typeof value === "number") return value;
|
|
2161
|
+
if (typeof value !== "string") return value;
|
|
2162
|
+
const trimmed = value.trim();
|
|
2163
|
+
if (trimmed.length === 0) return value;
|
|
2164
|
+
const num = Number(trimmed);
|
|
2165
|
+
return Number.isNaN(num) ? value : num;
|
|
2166
|
+
}
|
|
2167
|
+
function convertRegularValue(attr, element, vr) {
|
|
2101
2168
|
const valArray = toArray(attr.Value);
|
|
2102
2169
|
const values = [];
|
|
2170
|
+
const isNumeric = NUMERIC_JSON_VRS.has(vr);
|
|
2103
2171
|
for (const v of valArray) {
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
} else {
|
|
2107
|
-
values.push(v);
|
|
2108
|
-
}
|
|
2172
|
+
const unwrapped = unwrapValue(v);
|
|
2173
|
+
values.push(isNumeric ? coerceNumeric(unwrapped) : unwrapped);
|
|
2109
2174
|
}
|
|
2110
2175
|
if (values.length > 0) element.Value = values;
|
|
2111
2176
|
}
|
|
@@ -2122,7 +2187,7 @@ function convertElement(attr) {
|
|
|
2122
2187
|
} else if (element.vr === "SQ" && attr.Item !== void 0) {
|
|
2123
2188
|
convertSequence(attr, element);
|
|
2124
2189
|
} else if (attr.Value !== void 0) {
|
|
2125
|
-
convertRegularValue(attr, element);
|
|
2190
|
+
convertRegularValue(attr, element, vr);
|
|
2126
2191
|
}
|
|
2127
2192
|
return Object.freeze(element);
|
|
2128
2193
|
}
|
|
@@ -2701,8 +2766,9 @@ var DEFAULT_MAX_POOL_SIZE = 10;
|
|
|
2701
2766
|
var DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
|
|
2702
2767
|
var CONNECTION_RETRY_INTERVAL_MS = 500;
|
|
2703
2768
|
var MAX_CONNECTION_RETRIES = 200;
|
|
2769
|
+
var MAX_OUTPUT_LINES_PER_ASSOCIATION = 500;
|
|
2704
2770
|
var DicomReceiverOptionsSchema = zod.z.object({
|
|
2705
|
-
port: zod.z.number().int().min(
|
|
2771
|
+
port: zod.z.number().int().min(0).max(65535),
|
|
2706
2772
|
storageDir: zod.z.string().min(1).refine(isSafePath, { message: "path traversal detected in storageDir" }),
|
|
2707
2773
|
aeTitle: zod.z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
|
|
2708
2774
|
minPoolSize: zod.z.number().int().min(1).max(100).optional(),
|
|
@@ -2710,6 +2776,9 @@ var DicomReceiverOptionsSchema = zod.z.object({
|
|
|
2710
2776
|
connectionTimeoutMs: zod.z.number().int().positive().optional(),
|
|
2711
2777
|
configFile: zod.z.string().min(1).refine(isSafePath, { message: "path traversal detected in configFile" }).optional(),
|
|
2712
2778
|
configProfile: zod.z.string().min(1).optional(),
|
|
2779
|
+
acseTimeout: zod.z.number().int().positive().optional(),
|
|
2780
|
+
dimseTimeout: zod.z.number().int().positive().optional(),
|
|
2781
|
+
maxPdu: zod.z.number().int().min(4096).max(131072).optional(),
|
|
2713
2782
|
signal: zod.z.instanceof(AbortSignal).optional()
|
|
2714
2783
|
}).strict().refine((data) => (data.minPoolSize ?? DEFAULT_MIN_POOL_SIZE) <= (data.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE), {
|
|
2715
2784
|
message: "minPoolSize must be <= maxPoolSize"
|
|
@@ -2838,6 +2907,57 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
2838
2907
|
onAssociationComplete(listener) {
|
|
2839
2908
|
return this.on("ASSOCIATION_COMPLETE", listener);
|
|
2840
2909
|
}
|
|
2910
|
+
/**
|
|
2911
|
+
* Registers a listener for new associations (bubbled from workers).
|
|
2912
|
+
*
|
|
2913
|
+
* @param listener - Callback receiving association received data
|
|
2914
|
+
* @returns this for chaining
|
|
2915
|
+
*/
|
|
2916
|
+
onAssociationReceived(listener) {
|
|
2917
|
+
return this.on("ASSOCIATION_RECEIVED", listener);
|
|
2918
|
+
}
|
|
2919
|
+
/**
|
|
2920
|
+
* Registers a listener for C-STORE requests (bubbled from workers).
|
|
2921
|
+
*
|
|
2922
|
+
* @param listener - Callback receiving C-STORE request data
|
|
2923
|
+
* @returns this for chaining
|
|
2924
|
+
*/
|
|
2925
|
+
onCStoreRequest(listener) {
|
|
2926
|
+
return this.on("C_STORE_REQUEST", listener);
|
|
2927
|
+
}
|
|
2928
|
+
/**
|
|
2929
|
+
* Registers a listener for C-ECHO requests (bubbled from workers).
|
|
2930
|
+
*
|
|
2931
|
+
* @param listener - Callback receiving echo request data
|
|
2932
|
+
* @returns this for chaining
|
|
2933
|
+
*/
|
|
2934
|
+
onEchoRequest(listener) {
|
|
2935
|
+
return this.on("ECHO_REQUEST", listener);
|
|
2936
|
+
}
|
|
2937
|
+
/**
|
|
2938
|
+
* Registers a listener for refused associations (bubbled from workers).
|
|
2939
|
+
*
|
|
2940
|
+
* @param listener - Callback receiving refusing association data
|
|
2941
|
+
* @returns this for chaining
|
|
2942
|
+
*/
|
|
2943
|
+
onRefusingAssociation(listener) {
|
|
2944
|
+
return this.on("REFUSING_ASSOCIATION", listener);
|
|
2945
|
+
}
|
|
2946
|
+
/**
|
|
2947
|
+
* Routes an external socket to an idle worker.
|
|
2948
|
+
*
|
|
2949
|
+
* The pool must be started via `start()` before calling this method.
|
|
2950
|
+
* Use this when managing your own TCP listener (e.g., protocol router).
|
|
2951
|
+
*
|
|
2952
|
+
* @param socket - An incoming net.Socket to route to a worker
|
|
2953
|
+
*/
|
|
2954
|
+
handleSocket(socket) {
|
|
2955
|
+
if (!this.started) {
|
|
2956
|
+
socket.destroy(new Error("DicomReceiver: not started"));
|
|
2957
|
+
return;
|
|
2958
|
+
}
|
|
2959
|
+
void this.handleConnection(socket);
|
|
2960
|
+
}
|
|
2841
2961
|
/** Current pool status. */
|
|
2842
2962
|
get poolStatus() {
|
|
2843
2963
|
let idle = 0;
|
|
@@ -2851,8 +2971,11 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
2851
2971
|
// -----------------------------------------------------------------------
|
|
2852
2972
|
// TCP proxy
|
|
2853
2973
|
// -----------------------------------------------------------------------
|
|
2854
|
-
/** Starts the TCP proxy on the configured port. */
|
|
2974
|
+
/** Starts the TCP proxy on the configured port. Skips if port is 0. */
|
|
2855
2975
|
startTcpProxy() {
|
|
2976
|
+
if (this.options.port === 0) {
|
|
2977
|
+
return Promise.resolve(ok(void 0));
|
|
2978
|
+
}
|
|
2856
2979
|
return new Promise((resolve) => {
|
|
2857
2980
|
this.tcpServer = net__namespace.createServer((socket) => {
|
|
2858
2981
|
void this.handleConnection(socket);
|
|
@@ -2905,6 +3028,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
2905
3028
|
worker.associationDir = associationDir;
|
|
2906
3029
|
worker.files = [];
|
|
2907
3030
|
worker.fileSizes = [];
|
|
3031
|
+
worker.outputLines = [];
|
|
2908
3032
|
worker.startAt = Date.now();
|
|
2909
3033
|
this.pipeConnection(worker, remoteSocket);
|
|
2910
3034
|
void this.replenishPool();
|
|
@@ -2963,6 +3087,19 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
2963
3087
|
}
|
|
2964
3088
|
return ok(void 0);
|
|
2965
3089
|
}
|
|
3090
|
+
/** Creates a Dcmrecv instance with the pool's shared options. */
|
|
3091
|
+
createDcmrecv(port, tempDir) {
|
|
3092
|
+
return Dcmrecv.create({
|
|
3093
|
+
port,
|
|
3094
|
+
aeTitle: this.options.aeTitle ?? "DCMRECV",
|
|
3095
|
+
outputDirectory: tempDir,
|
|
3096
|
+
configFile: this.options.configFile,
|
|
3097
|
+
configProfile: this.options.configProfile,
|
|
3098
|
+
acseTimeout: this.options.acseTimeout,
|
|
3099
|
+
dimseTimeout: this.options.dimseTimeout,
|
|
3100
|
+
maxPdu: this.options.maxPdu
|
|
3101
|
+
});
|
|
3102
|
+
}
|
|
2966
3103
|
/** Spawns a single Dcmrecv worker with an ephemeral port. */
|
|
2967
3104
|
async spawnWorker() {
|
|
2968
3105
|
const portResult = await allocatePort();
|
|
@@ -2971,13 +3108,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
2971
3108
|
const tempDir = path__namespace.join(os__namespace.tmpdir(), `dcmrecv-pool-${String(port)}-${String(Date.now())}`);
|
|
2972
3109
|
const mkdirResult = await ensureDirectory(tempDir);
|
|
2973
3110
|
if (!mkdirResult.ok) return mkdirResult;
|
|
2974
|
-
const createResult =
|
|
2975
|
-
port,
|
|
2976
|
-
aeTitle: this.options.aeTitle ?? "DCMRECV",
|
|
2977
|
-
outputDirectory: tempDir,
|
|
2978
|
-
configFile: this.options.configFile,
|
|
2979
|
-
configProfile: this.options.configProfile
|
|
2980
|
-
});
|
|
3111
|
+
const createResult = this.createDcmrecv(port, tempDir);
|
|
2981
3112
|
if (!createResult.ok) return createResult;
|
|
2982
3113
|
const dcmrecv = createResult.value;
|
|
2983
3114
|
const worker = {
|
|
@@ -2989,6 +3120,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
2989
3120
|
associationDir: void 0,
|
|
2990
3121
|
files: [],
|
|
2991
3122
|
fileSizes: [],
|
|
3123
|
+
outputLines: [],
|
|
2992
3124
|
startAt: void 0,
|
|
2993
3125
|
remoteSocket: void 0,
|
|
2994
3126
|
workerSocket: void 0
|
|
@@ -3050,10 +3182,15 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3050
3182
|
// -----------------------------------------------------------------------
|
|
3051
3183
|
// Worker event wiring
|
|
3052
3184
|
// -----------------------------------------------------------------------
|
|
3053
|
-
/** Wires
|
|
3185
|
+
/** Wires all events on a worker: file handling, association lifecycle, output capture. */
|
|
3054
3186
|
wireWorkerEvents(worker) {
|
|
3055
3187
|
this.wireFileReceived(worker);
|
|
3056
3188
|
this.wireAssociationComplete(worker);
|
|
3189
|
+
this.wireAssociationReceived(worker);
|
|
3190
|
+
this.wireCStoreRequest(worker);
|
|
3191
|
+
this.wireEchoRequest(worker);
|
|
3192
|
+
this.wireRefusingAssociation(worker);
|
|
3193
|
+
this.wireOutputCapture(worker);
|
|
3057
3194
|
}
|
|
3058
3195
|
/** Wires FILE_RECEIVED from dcmrecv worker to handleFileReceived. */
|
|
3059
3196
|
wireFileReceived(worker) {
|
|
@@ -3101,6 +3238,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3101
3238
|
const assocId = worker.associationId ?? data.associationId;
|
|
3102
3239
|
const assocDir = worker.associationDir ?? "";
|
|
3103
3240
|
const files = [...worker.files];
|
|
3241
|
+
const output = [...worker.outputLines];
|
|
3104
3242
|
const endAt = Date.now();
|
|
3105
3243
|
const startAt = worker.startAt ?? endAt;
|
|
3106
3244
|
const totalBytes = sumArray(worker.fileSizes);
|
|
@@ -3118,19 +3256,65 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3118
3256
|
totalBytes,
|
|
3119
3257
|
bytesPerSecond,
|
|
3120
3258
|
startAt,
|
|
3121
|
-
endAt
|
|
3259
|
+
endAt,
|
|
3260
|
+
output
|
|
3122
3261
|
});
|
|
3123
3262
|
worker.state = "idle";
|
|
3124
3263
|
worker.associationId = void 0;
|
|
3125
3264
|
worker.associationDir = void 0;
|
|
3126
3265
|
worker.files = [];
|
|
3127
3266
|
worker.fileSizes = [];
|
|
3267
|
+
worker.outputLines = [];
|
|
3128
3268
|
worker.startAt = void 0;
|
|
3129
3269
|
worker.remoteSocket = void 0;
|
|
3130
3270
|
worker.workerSocket = void 0;
|
|
3131
3271
|
void this.scaleDown();
|
|
3132
3272
|
});
|
|
3133
3273
|
}
|
|
3274
|
+
/** Bubbles ASSOCIATION_RECEIVED from dcmrecv worker. */
|
|
3275
|
+
wireAssociationReceived(worker) {
|
|
3276
|
+
worker.dcmrecv.onEvent("ASSOCIATION_RECEIVED", (data) => {
|
|
3277
|
+
this.emit("ASSOCIATION_RECEIVED", {
|
|
3278
|
+
associationId: worker.associationId ?? "",
|
|
3279
|
+
callingAE: data.callingAE,
|
|
3280
|
+
calledAE: data.calledAE,
|
|
3281
|
+
source: data.source
|
|
3282
|
+
});
|
|
3283
|
+
});
|
|
3284
|
+
}
|
|
3285
|
+
/** Bubbles C_STORE_REQUEST from dcmrecv worker. */
|
|
3286
|
+
wireCStoreRequest(worker) {
|
|
3287
|
+
worker.dcmrecv.onEvent("C_STORE_REQUEST", (data) => {
|
|
3288
|
+
this.emit("C_STORE_REQUEST", {
|
|
3289
|
+
associationId: worker.associationId ?? "",
|
|
3290
|
+
raw: data.raw
|
|
3291
|
+
});
|
|
3292
|
+
});
|
|
3293
|
+
}
|
|
3294
|
+
/** Bubbles ECHO_REQUEST from dcmrecv worker. */
|
|
3295
|
+
wireEchoRequest(worker) {
|
|
3296
|
+
worker.dcmrecv.onEvent("ECHO_REQUEST", () => {
|
|
3297
|
+
this.emit("ECHO_REQUEST", {
|
|
3298
|
+
associationId: worker.associationId ?? ""
|
|
3299
|
+
});
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
3302
|
+
/** Bubbles REFUSING_ASSOCIATION from dcmrecv worker. */
|
|
3303
|
+
wireRefusingAssociation(worker) {
|
|
3304
|
+
worker.dcmrecv.onEvent("REFUSING_ASSOCIATION", (data) => {
|
|
3305
|
+
this.emit("REFUSING_ASSOCIATION", {
|
|
3306
|
+
reason: data.reason
|
|
3307
|
+
});
|
|
3308
|
+
});
|
|
3309
|
+
}
|
|
3310
|
+
/** Captures worker output lines during busy associations. */
|
|
3311
|
+
wireOutputCapture(worker) {
|
|
3312
|
+
worker.dcmrecv.on("line", ({ text }) => {
|
|
3313
|
+
if (worker.state === "busy" && worker.outputLines.length < MAX_OUTPUT_LINES_PER_ASSOCIATION) {
|
|
3314
|
+
worker.outputLines.push(text);
|
|
3315
|
+
}
|
|
3316
|
+
});
|
|
3317
|
+
}
|
|
3134
3318
|
// -----------------------------------------------------------------------
|
|
3135
3319
|
// Abort signal
|
|
3136
3320
|
// -----------------------------------------------------------------------
|