@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.js
CHANGED
|
@@ -1531,6 +1531,45 @@ var ChangeSet = class _ChangeSet {
|
|
|
1531
1531
|
return result;
|
|
1532
1532
|
}
|
|
1533
1533
|
};
|
|
1534
|
+
|
|
1535
|
+
// src/dicom/walkTags.ts
|
|
1536
|
+
var DEFAULT_MAX_DEPTH = 16;
|
|
1537
|
+
function walkTags(data, options) {
|
|
1538
|
+
const ctx = {
|
|
1539
|
+
maxDepth: options?.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
1540
|
+
vrSet: options?.vrFilter !== void 0 ? new Set(options.vrFilter) : void 0,
|
|
1541
|
+
results: []
|
|
1542
|
+
};
|
|
1543
|
+
walkLevel(data, 0, "", ctx);
|
|
1544
|
+
return ctx.results;
|
|
1545
|
+
}
|
|
1546
|
+
function walkLevel(data, depth, pathPrefix, ctx) {
|
|
1547
|
+
const keys = Object.keys(data);
|
|
1548
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1549
|
+
const tag = keys[i];
|
|
1550
|
+
if (tag === void 0) continue;
|
|
1551
|
+
const element = data[tag];
|
|
1552
|
+
if (element === void 0) continue;
|
|
1553
|
+
const vr = element.vr;
|
|
1554
|
+
const path2 = pathPrefix.length > 0 ? `${pathPrefix}.${tag}` : tag;
|
|
1555
|
+
if (ctx.vrSet === void 0 || ctx.vrSet.has(vr)) {
|
|
1556
|
+
ctx.results.push({ tag, element, vr, depth, path: path2 });
|
|
1557
|
+
}
|
|
1558
|
+
if (vr === "SQ" && element.Value !== void 0 && depth < ctx.maxDepth) {
|
|
1559
|
+
walkSequenceItems(element.Value, depth, path2, ctx);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
function walkSequenceItems(items, depth, parentPath, ctx) {
|
|
1564
|
+
for (let idx = 0; idx < items.length; idx++) {
|
|
1565
|
+
const item = items[idx];
|
|
1566
|
+
if (typeof item !== "object" || item === null) continue;
|
|
1567
|
+
const itemPath = `${parentPath}[${idx}]`;
|
|
1568
|
+
walkLevel(item, depth + 1, itemPath, ctx);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// src/dicom/DicomDataset.ts
|
|
1534
1573
|
var HEX_TAG_KEY = /^[0-9A-Fa-f]{8}$/;
|
|
1535
1574
|
function isDicomJsonElement(value) {
|
|
1536
1575
|
return typeof value === "object" && value !== null && "vr" in value && typeof value["vr"] === "string";
|
|
@@ -1859,6 +1898,15 @@ var DicomDataset = class _DicomDataset {
|
|
|
1859
1898
|
const result = collectWildcard(this.data, segments);
|
|
1860
1899
|
return result.values;
|
|
1861
1900
|
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Walks all tags in this dataset, optionally filtering by VR and recursing into sequences.
|
|
1903
|
+
*
|
|
1904
|
+
* @param options - Optional VR filter and max depth
|
|
1905
|
+
* @returns A readonly array of all matching tag entries
|
|
1906
|
+
*/
|
|
1907
|
+
walkTags(options) {
|
|
1908
|
+
return walkTags(this.data, options);
|
|
1909
|
+
}
|
|
1862
1910
|
// -----------------------------------------------------------------------
|
|
1863
1911
|
// Convenience readonly getters
|
|
1864
1912
|
// -----------------------------------------------------------------------
|
|
@@ -2071,15 +2119,32 @@ function convertSequence(attr, element) {
|
|
|
2071
2119
|
}
|
|
2072
2120
|
if (values.length > 0) element.Value = values;
|
|
2073
2121
|
}
|
|
2074
|
-
|
|
2122
|
+
var NUMERIC_JSON_VRS = /* @__PURE__ */ new Set(["DS", "FL", "FD", "IS", "SL", "SS", "SV", "UL", "US", "UV"]);
|
|
2123
|
+
function unwrapValue(v) {
|
|
2124
|
+
if (typeof v !== "object" || v === null) return v;
|
|
2125
|
+
const obj = v;
|
|
2126
|
+
if ("#text" in obj) return obj["#text"];
|
|
2127
|
+
const keys = Object.keys(obj);
|
|
2128
|
+
if (keys.length === 1 && keys[0] !== void 0 && keys[0].startsWith("@_")) {
|
|
2129
|
+
return obj[keys[0]];
|
|
2130
|
+
}
|
|
2131
|
+
return v;
|
|
2132
|
+
}
|
|
2133
|
+
function coerceNumeric(value) {
|
|
2134
|
+
if (typeof value === "number") return value;
|
|
2135
|
+
if (typeof value !== "string") return value;
|
|
2136
|
+
const trimmed = value.trim();
|
|
2137
|
+
if (trimmed.length === 0) return value;
|
|
2138
|
+
const num = Number(trimmed);
|
|
2139
|
+
return Number.isNaN(num) ? value : num;
|
|
2140
|
+
}
|
|
2141
|
+
function convertRegularValue(attr, element, vr) {
|
|
2075
2142
|
const valArray = toArray(attr.Value);
|
|
2076
2143
|
const values = [];
|
|
2144
|
+
const isNumeric = NUMERIC_JSON_VRS.has(vr);
|
|
2077
2145
|
for (const v of valArray) {
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
} else {
|
|
2081
|
-
values.push(v);
|
|
2082
|
-
}
|
|
2146
|
+
const unwrapped = unwrapValue(v);
|
|
2147
|
+
values.push(isNumeric ? coerceNumeric(unwrapped) : unwrapped);
|
|
2083
2148
|
}
|
|
2084
2149
|
if (values.length > 0) element.Value = values;
|
|
2085
2150
|
}
|
|
@@ -2096,7 +2161,7 @@ function convertElement(attr) {
|
|
|
2096
2161
|
} else if (element.vr === "SQ" && attr.Item !== void 0) {
|
|
2097
2162
|
convertSequence(attr, element);
|
|
2098
2163
|
} else if (attr.Value !== void 0) {
|
|
2099
|
-
convertRegularValue(attr, element);
|
|
2164
|
+
convertRegularValue(attr, element, vr);
|
|
2100
2165
|
}
|
|
2101
2166
|
return Object.freeze(element);
|
|
2102
2167
|
}
|
|
@@ -2675,8 +2740,9 @@ var DEFAULT_MAX_POOL_SIZE = 10;
|
|
|
2675
2740
|
var DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
|
|
2676
2741
|
var CONNECTION_RETRY_INTERVAL_MS = 500;
|
|
2677
2742
|
var MAX_CONNECTION_RETRIES = 200;
|
|
2743
|
+
var MAX_OUTPUT_LINES_PER_ASSOCIATION = 500;
|
|
2678
2744
|
var DicomReceiverOptionsSchema = z.object({
|
|
2679
|
-
port: z.number().int().min(
|
|
2745
|
+
port: z.number().int().min(0).max(65535),
|
|
2680
2746
|
storageDir: z.string().min(1).refine(isSafePath, { message: "path traversal detected in storageDir" }),
|
|
2681
2747
|
aeTitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
|
|
2682
2748
|
minPoolSize: z.number().int().min(1).max(100).optional(),
|
|
@@ -2684,6 +2750,9 @@ var DicomReceiverOptionsSchema = z.object({
|
|
|
2684
2750
|
connectionTimeoutMs: z.number().int().positive().optional(),
|
|
2685
2751
|
configFile: z.string().min(1).refine(isSafePath, { message: "path traversal detected in configFile" }).optional(),
|
|
2686
2752
|
configProfile: z.string().min(1).optional(),
|
|
2753
|
+
acseTimeout: z.number().int().positive().optional(),
|
|
2754
|
+
dimseTimeout: z.number().int().positive().optional(),
|
|
2755
|
+
maxPdu: z.number().int().min(4096).max(131072).optional(),
|
|
2687
2756
|
signal: z.instanceof(AbortSignal).optional()
|
|
2688
2757
|
}).strict().refine((data) => (data.minPoolSize ?? DEFAULT_MIN_POOL_SIZE) <= (data.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE), {
|
|
2689
2758
|
message: "minPoolSize must be <= maxPoolSize"
|
|
@@ -2812,6 +2881,57 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
2812
2881
|
onAssociationComplete(listener) {
|
|
2813
2882
|
return this.on("ASSOCIATION_COMPLETE", listener);
|
|
2814
2883
|
}
|
|
2884
|
+
/**
|
|
2885
|
+
* Registers a listener for new associations (bubbled from workers).
|
|
2886
|
+
*
|
|
2887
|
+
* @param listener - Callback receiving association received data
|
|
2888
|
+
* @returns this for chaining
|
|
2889
|
+
*/
|
|
2890
|
+
onAssociationReceived(listener) {
|
|
2891
|
+
return this.on("ASSOCIATION_RECEIVED", listener);
|
|
2892
|
+
}
|
|
2893
|
+
/**
|
|
2894
|
+
* Registers a listener for C-STORE requests (bubbled from workers).
|
|
2895
|
+
*
|
|
2896
|
+
* @param listener - Callback receiving C-STORE request data
|
|
2897
|
+
* @returns this for chaining
|
|
2898
|
+
*/
|
|
2899
|
+
onCStoreRequest(listener) {
|
|
2900
|
+
return this.on("C_STORE_REQUEST", listener);
|
|
2901
|
+
}
|
|
2902
|
+
/**
|
|
2903
|
+
* Registers a listener for C-ECHO requests (bubbled from workers).
|
|
2904
|
+
*
|
|
2905
|
+
* @param listener - Callback receiving echo request data
|
|
2906
|
+
* @returns this for chaining
|
|
2907
|
+
*/
|
|
2908
|
+
onEchoRequest(listener) {
|
|
2909
|
+
return this.on("ECHO_REQUEST", listener);
|
|
2910
|
+
}
|
|
2911
|
+
/**
|
|
2912
|
+
* Registers a listener for refused associations (bubbled from workers).
|
|
2913
|
+
*
|
|
2914
|
+
* @param listener - Callback receiving refusing association data
|
|
2915
|
+
* @returns this for chaining
|
|
2916
|
+
*/
|
|
2917
|
+
onRefusingAssociation(listener) {
|
|
2918
|
+
return this.on("REFUSING_ASSOCIATION", listener);
|
|
2919
|
+
}
|
|
2920
|
+
/**
|
|
2921
|
+
* Routes an external socket to an idle worker.
|
|
2922
|
+
*
|
|
2923
|
+
* The pool must be started via `start()` before calling this method.
|
|
2924
|
+
* Use this when managing your own TCP listener (e.g., protocol router).
|
|
2925
|
+
*
|
|
2926
|
+
* @param socket - An incoming net.Socket to route to a worker
|
|
2927
|
+
*/
|
|
2928
|
+
handleSocket(socket) {
|
|
2929
|
+
if (!this.started) {
|
|
2930
|
+
socket.destroy(new Error("DicomReceiver: not started"));
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
void this.handleConnection(socket);
|
|
2934
|
+
}
|
|
2815
2935
|
/** Current pool status. */
|
|
2816
2936
|
get poolStatus() {
|
|
2817
2937
|
let idle = 0;
|
|
@@ -2825,8 +2945,11 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
2825
2945
|
// -----------------------------------------------------------------------
|
|
2826
2946
|
// TCP proxy
|
|
2827
2947
|
// -----------------------------------------------------------------------
|
|
2828
|
-
/** Starts the TCP proxy on the configured port. */
|
|
2948
|
+
/** Starts the TCP proxy on the configured port. Skips if port is 0. */
|
|
2829
2949
|
startTcpProxy() {
|
|
2950
|
+
if (this.options.port === 0) {
|
|
2951
|
+
return Promise.resolve(ok(void 0));
|
|
2952
|
+
}
|
|
2830
2953
|
return new Promise((resolve) => {
|
|
2831
2954
|
this.tcpServer = net.createServer((socket) => {
|
|
2832
2955
|
void this.handleConnection(socket);
|
|
@@ -2879,6 +3002,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
2879
3002
|
worker.associationDir = associationDir;
|
|
2880
3003
|
worker.files = [];
|
|
2881
3004
|
worker.fileSizes = [];
|
|
3005
|
+
worker.outputLines = [];
|
|
2882
3006
|
worker.startAt = Date.now();
|
|
2883
3007
|
this.pipeConnection(worker, remoteSocket);
|
|
2884
3008
|
void this.replenishPool();
|
|
@@ -2937,6 +3061,19 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
2937
3061
|
}
|
|
2938
3062
|
return ok(void 0);
|
|
2939
3063
|
}
|
|
3064
|
+
/** Creates a Dcmrecv instance with the pool's shared options. */
|
|
3065
|
+
createDcmrecv(port, tempDir) {
|
|
3066
|
+
return Dcmrecv.create({
|
|
3067
|
+
port,
|
|
3068
|
+
aeTitle: this.options.aeTitle ?? "DCMRECV",
|
|
3069
|
+
outputDirectory: tempDir,
|
|
3070
|
+
configFile: this.options.configFile,
|
|
3071
|
+
configProfile: this.options.configProfile,
|
|
3072
|
+
acseTimeout: this.options.acseTimeout,
|
|
3073
|
+
dimseTimeout: this.options.dimseTimeout,
|
|
3074
|
+
maxPdu: this.options.maxPdu
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
2940
3077
|
/** Spawns a single Dcmrecv worker with an ephemeral port. */
|
|
2941
3078
|
async spawnWorker() {
|
|
2942
3079
|
const portResult = await allocatePort();
|
|
@@ -2945,13 +3082,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
2945
3082
|
const tempDir = path.join(os.tmpdir(), `dcmrecv-pool-${String(port)}-${String(Date.now())}`);
|
|
2946
3083
|
const mkdirResult = await ensureDirectory(tempDir);
|
|
2947
3084
|
if (!mkdirResult.ok) return mkdirResult;
|
|
2948
|
-
const createResult =
|
|
2949
|
-
port,
|
|
2950
|
-
aeTitle: this.options.aeTitle ?? "DCMRECV",
|
|
2951
|
-
outputDirectory: tempDir,
|
|
2952
|
-
configFile: this.options.configFile,
|
|
2953
|
-
configProfile: this.options.configProfile
|
|
2954
|
-
});
|
|
3085
|
+
const createResult = this.createDcmrecv(port, tempDir);
|
|
2955
3086
|
if (!createResult.ok) return createResult;
|
|
2956
3087
|
const dcmrecv = createResult.value;
|
|
2957
3088
|
const worker = {
|
|
@@ -2963,6 +3094,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
2963
3094
|
associationDir: void 0,
|
|
2964
3095
|
files: [],
|
|
2965
3096
|
fileSizes: [],
|
|
3097
|
+
outputLines: [],
|
|
2966
3098
|
startAt: void 0,
|
|
2967
3099
|
remoteSocket: void 0,
|
|
2968
3100
|
workerSocket: void 0
|
|
@@ -3024,10 +3156,15 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3024
3156
|
// -----------------------------------------------------------------------
|
|
3025
3157
|
// Worker event wiring
|
|
3026
3158
|
// -----------------------------------------------------------------------
|
|
3027
|
-
/** Wires
|
|
3159
|
+
/** Wires all events on a worker: file handling, association lifecycle, output capture. */
|
|
3028
3160
|
wireWorkerEvents(worker) {
|
|
3029
3161
|
this.wireFileReceived(worker);
|
|
3030
3162
|
this.wireAssociationComplete(worker);
|
|
3163
|
+
this.wireAssociationReceived(worker);
|
|
3164
|
+
this.wireCStoreRequest(worker);
|
|
3165
|
+
this.wireEchoRequest(worker);
|
|
3166
|
+
this.wireRefusingAssociation(worker);
|
|
3167
|
+
this.wireOutputCapture(worker);
|
|
3031
3168
|
}
|
|
3032
3169
|
/** Wires FILE_RECEIVED from dcmrecv worker to handleFileReceived. */
|
|
3033
3170
|
wireFileReceived(worker) {
|
|
@@ -3075,6 +3212,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3075
3212
|
const assocId = worker.associationId ?? data.associationId;
|
|
3076
3213
|
const assocDir = worker.associationDir ?? "";
|
|
3077
3214
|
const files = [...worker.files];
|
|
3215
|
+
const output = [...worker.outputLines];
|
|
3078
3216
|
const endAt = Date.now();
|
|
3079
3217
|
const startAt = worker.startAt ?? endAt;
|
|
3080
3218
|
const totalBytes = sumArray(worker.fileSizes);
|
|
@@ -3092,19 +3230,65 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3092
3230
|
totalBytes,
|
|
3093
3231
|
bytesPerSecond,
|
|
3094
3232
|
startAt,
|
|
3095
|
-
endAt
|
|
3233
|
+
endAt,
|
|
3234
|
+
output
|
|
3096
3235
|
});
|
|
3097
3236
|
worker.state = "idle";
|
|
3098
3237
|
worker.associationId = void 0;
|
|
3099
3238
|
worker.associationDir = void 0;
|
|
3100
3239
|
worker.files = [];
|
|
3101
3240
|
worker.fileSizes = [];
|
|
3241
|
+
worker.outputLines = [];
|
|
3102
3242
|
worker.startAt = void 0;
|
|
3103
3243
|
worker.remoteSocket = void 0;
|
|
3104
3244
|
worker.workerSocket = void 0;
|
|
3105
3245
|
void this.scaleDown();
|
|
3106
3246
|
});
|
|
3107
3247
|
}
|
|
3248
|
+
/** Bubbles ASSOCIATION_RECEIVED from dcmrecv worker. */
|
|
3249
|
+
wireAssociationReceived(worker) {
|
|
3250
|
+
worker.dcmrecv.onEvent("ASSOCIATION_RECEIVED", (data) => {
|
|
3251
|
+
this.emit("ASSOCIATION_RECEIVED", {
|
|
3252
|
+
associationId: worker.associationId ?? "",
|
|
3253
|
+
callingAE: data.callingAE,
|
|
3254
|
+
calledAE: data.calledAE,
|
|
3255
|
+
source: data.source
|
|
3256
|
+
});
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3259
|
+
/** Bubbles C_STORE_REQUEST from dcmrecv worker. */
|
|
3260
|
+
wireCStoreRequest(worker) {
|
|
3261
|
+
worker.dcmrecv.onEvent("C_STORE_REQUEST", (data) => {
|
|
3262
|
+
this.emit("C_STORE_REQUEST", {
|
|
3263
|
+
associationId: worker.associationId ?? "",
|
|
3264
|
+
raw: data.raw
|
|
3265
|
+
});
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
/** Bubbles ECHO_REQUEST from dcmrecv worker. */
|
|
3269
|
+
wireEchoRequest(worker) {
|
|
3270
|
+
worker.dcmrecv.onEvent("ECHO_REQUEST", () => {
|
|
3271
|
+
this.emit("ECHO_REQUEST", {
|
|
3272
|
+
associationId: worker.associationId ?? ""
|
|
3273
|
+
});
|
|
3274
|
+
});
|
|
3275
|
+
}
|
|
3276
|
+
/** Bubbles REFUSING_ASSOCIATION from dcmrecv worker. */
|
|
3277
|
+
wireRefusingAssociation(worker) {
|
|
3278
|
+
worker.dcmrecv.onEvent("REFUSING_ASSOCIATION", (data) => {
|
|
3279
|
+
this.emit("REFUSING_ASSOCIATION", {
|
|
3280
|
+
reason: data.reason
|
|
3281
|
+
});
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
/** Captures worker output lines during busy associations. */
|
|
3285
|
+
wireOutputCapture(worker) {
|
|
3286
|
+
worker.dcmrecv.on("line", ({ text }) => {
|
|
3287
|
+
if (worker.state === "busy" && worker.outputLines.length < MAX_OUTPUT_LINES_PER_ASSOCIATION) {
|
|
3288
|
+
worker.outputLines.push(text);
|
|
3289
|
+
}
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3108
3292
|
// -----------------------------------------------------------------------
|
|
3109
3293
|
// Abort signal
|
|
3110
3294
|
// -----------------------------------------------------------------------
|