@ubercode/dcmtk 0.3.0 → 0.5.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 +4 -1
- package/dist/{DicomInstance-D9plqHp5.d.ts → DicomInstance-CGBr3a-C.d.ts} +1 -1
- package/dist/{DicomInstance-DNHPkkzl.d.cts → DicomInstance-DWOjhccQ.d.cts} +1 -1
- package/dist/{dcmodify-Gds9u5Vj.d.cts → dcmodify-B9js5K1f.d.cts} +2 -0
- package/dist/{dcmodify-B-_uUIKB.d.ts → dcmodify-BvaIeyJg.d.ts} +2 -0
- package/dist/dicom.cjs +27 -11
- 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 +27 -11
- package/dist/dicom.js.map +1 -1
- package/dist/index-CsYd7SMF.d.ts +1735 -0
- package/dist/index-DbUfqT7o.d.cts +1735 -0
- package/dist/index.cjs +896 -179
- 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 +896 -179
- package/dist/index.js.map +1 -1
- package/dist/servers.cjs +157 -22
- 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 +157 -22
- package/dist/servers.js.map +1 -1
- package/dist/tools.cjs +754 -156
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +226 -10
- package/dist/tools.d.ts +226 -10
- package/dist/tools.js +754 -156
- package/dist/tools.js.map +1 -1
- package/package.json +17 -1
package/dist/servers.js
CHANGED
|
@@ -2175,19 +2175,28 @@ function repairJson(raw) {
|
|
|
2175
2175
|
var Dcm2jsonOptionsSchema = z.object({
|
|
2176
2176
|
timeoutMs: z.number().int().positive().optional(),
|
|
2177
2177
|
signal: z.instanceof(AbortSignal).optional(),
|
|
2178
|
-
directOnly: z.boolean().optional()
|
|
2178
|
+
directOnly: z.boolean().optional(),
|
|
2179
|
+
verbosity: z.enum(["verbose", "debug"]).optional()
|
|
2179
2180
|
}).strict().optional();
|
|
2180
|
-
|
|
2181
|
+
var VERBOSITY_FLAGS = { verbose: "-v", debug: "-d" };
|
|
2182
|
+
function buildVerbosityArgs(verbosity) {
|
|
2183
|
+
if (verbosity !== void 0) {
|
|
2184
|
+
return [VERBOSITY_FLAGS[verbosity]];
|
|
2185
|
+
}
|
|
2186
|
+
return [];
|
|
2187
|
+
}
|
|
2188
|
+
async function tryXmlPath(inputPath, timeoutMs, signal, verbosity) {
|
|
2181
2189
|
const xmlBinary = resolveBinary("dcm2xml");
|
|
2182
2190
|
if (!xmlBinary.ok) {
|
|
2183
2191
|
return err(xmlBinary.error);
|
|
2184
2192
|
}
|
|
2185
|
-
const
|
|
2193
|
+
const xmlArgs = [...buildVerbosityArgs(verbosity), "-nat", inputPath];
|
|
2194
|
+
const xmlResult = await execCommand(xmlBinary.value, xmlArgs, { timeoutMs, signal });
|
|
2186
2195
|
if (!xmlResult.ok) {
|
|
2187
2196
|
return err(xmlResult.error);
|
|
2188
2197
|
}
|
|
2189
2198
|
if (xmlResult.value.exitCode !== 0) {
|
|
2190
|
-
return err(createToolError("dcm2xml",
|
|
2199
|
+
return err(createToolError("dcm2xml", xmlArgs, xmlResult.value.exitCode, xmlResult.value.stderr));
|
|
2191
2200
|
}
|
|
2192
2201
|
const jsonResult = xmlToJson(xmlResult.value.stdout);
|
|
2193
2202
|
if (!jsonResult.ok) {
|
|
@@ -2195,17 +2204,18 @@ async function tryXmlPath(inputPath, timeoutMs, signal) {
|
|
|
2195
2204
|
}
|
|
2196
2205
|
return ok({ data: jsonResult.value, source: "xml" });
|
|
2197
2206
|
}
|
|
2198
|
-
async function tryDirectPath(inputPath, timeoutMs, signal) {
|
|
2207
|
+
async function tryDirectPath(inputPath, timeoutMs, signal, verbosity) {
|
|
2199
2208
|
const jsonBinary = resolveBinary("dcm2json");
|
|
2200
2209
|
if (!jsonBinary.ok) {
|
|
2201
2210
|
return err(jsonBinary.error);
|
|
2202
2211
|
}
|
|
2203
|
-
const
|
|
2212
|
+
const directArgs = [...buildVerbosityArgs(verbosity), inputPath];
|
|
2213
|
+
const result = await execCommand(jsonBinary.value, directArgs, { timeoutMs, signal });
|
|
2204
2214
|
if (!result.ok) {
|
|
2205
2215
|
return err(result.error);
|
|
2206
2216
|
}
|
|
2207
2217
|
if (result.value.exitCode !== 0) {
|
|
2208
|
-
return err(createToolError("dcm2json",
|
|
2218
|
+
return err(createToolError("dcm2json", directArgs, result.value.exitCode, result.value.stderr));
|
|
2209
2219
|
}
|
|
2210
2220
|
try {
|
|
2211
2221
|
const repaired = repairJson(result.value.stdout);
|
|
@@ -2222,20 +2232,22 @@ async function dcm2json(inputPath, options) {
|
|
|
2222
2232
|
}
|
|
2223
2233
|
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2224
2234
|
const signal = options?.signal;
|
|
2235
|
+
const verbosity = options?.verbosity;
|
|
2225
2236
|
if (options?.directOnly === true) {
|
|
2226
|
-
return tryDirectPath(inputPath, timeoutMs, signal);
|
|
2237
|
+
return tryDirectPath(inputPath, timeoutMs, signal, verbosity);
|
|
2227
2238
|
}
|
|
2228
|
-
const xmlResult = await tryXmlPath(inputPath, timeoutMs, signal);
|
|
2239
|
+
const xmlResult = await tryXmlPath(inputPath, timeoutMs, signal, verbosity);
|
|
2229
2240
|
if (xmlResult.ok) {
|
|
2230
2241
|
return xmlResult;
|
|
2231
2242
|
}
|
|
2232
|
-
return tryDirectPath(inputPath, timeoutMs, signal);
|
|
2243
|
+
return tryDirectPath(inputPath, timeoutMs, signal, verbosity);
|
|
2233
2244
|
}
|
|
2234
2245
|
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+\])?)*)?$/;
|
|
2235
2246
|
var TagModificationSchema = z.object({
|
|
2236
2247
|
tag: z.string().regex(TAG_OR_PATH_PATTERN),
|
|
2237
2248
|
value: z.string()
|
|
2238
2249
|
});
|
|
2250
|
+
var VERBOSITY_FLAGS2 = { verbose: "-v", debug: "-d" };
|
|
2239
2251
|
var DcmodifyOptionsSchema = z.object({
|
|
2240
2252
|
timeoutMs: z.number().int().positive().optional(),
|
|
2241
2253
|
signal: z.instanceof(AbortSignal).optional(),
|
|
@@ -2244,12 +2256,16 @@ var DcmodifyOptionsSchema = z.object({
|
|
|
2244
2256
|
erasePrivateTags: z.boolean().optional(),
|
|
2245
2257
|
noBackup: z.boolean().optional(),
|
|
2246
2258
|
insertIfMissing: z.boolean().optional(),
|
|
2247
|
-
ignoreMissingTags: z.boolean().optional()
|
|
2259
|
+
ignoreMissingTags: z.boolean().optional(),
|
|
2260
|
+
verbosity: z.enum(["verbose", "debug"]).optional()
|
|
2248
2261
|
}).strict().refine((data) => data.modifications.length > 0 || data.erasures !== void 0 && data.erasures.length > 0 || data.erasePrivateTags === true, {
|
|
2249
2262
|
message: "At least one of modifications, erasures, or erasePrivateTags is required"
|
|
2250
2263
|
});
|
|
2251
2264
|
function buildArgs3(inputPath, options) {
|
|
2252
2265
|
const args = [];
|
|
2266
|
+
if (options.verbosity !== void 0) {
|
|
2267
|
+
args.push(VERBOSITY_FLAGS2[options.verbosity]);
|
|
2268
|
+
}
|
|
2253
2269
|
if (options.noBackup !== false) {
|
|
2254
2270
|
args.push("-nb");
|
|
2255
2271
|
}
|
|
@@ -2659,8 +2675,9 @@ var DEFAULT_MAX_POOL_SIZE = 10;
|
|
|
2659
2675
|
var DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
|
|
2660
2676
|
var CONNECTION_RETRY_INTERVAL_MS = 500;
|
|
2661
2677
|
var MAX_CONNECTION_RETRIES = 200;
|
|
2678
|
+
var MAX_OUTPUT_LINES_PER_ASSOCIATION = 500;
|
|
2662
2679
|
var DicomReceiverOptionsSchema = z.object({
|
|
2663
|
-
port: z.number().int().min(
|
|
2680
|
+
port: z.number().int().min(0).max(65535),
|
|
2664
2681
|
storageDir: z.string().min(1).refine(isSafePath, { message: "path traversal detected in storageDir" }),
|
|
2665
2682
|
aeTitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
|
|
2666
2683
|
minPoolSize: z.number().int().min(1).max(100).optional(),
|
|
@@ -2668,6 +2685,9 @@ var DicomReceiverOptionsSchema = z.object({
|
|
|
2668
2685
|
connectionTimeoutMs: z.number().int().positive().optional(),
|
|
2669
2686
|
configFile: z.string().min(1).refine(isSafePath, { message: "path traversal detected in configFile" }).optional(),
|
|
2670
2687
|
configProfile: z.string().min(1).optional(),
|
|
2688
|
+
acseTimeout: z.number().int().positive().optional(),
|
|
2689
|
+
dimseTimeout: z.number().int().positive().optional(),
|
|
2690
|
+
maxPdu: z.number().int().min(4096).max(131072).optional(),
|
|
2671
2691
|
signal: z.instanceof(AbortSignal).optional()
|
|
2672
2692
|
}).strict().refine((data) => (data.minPoolSize ?? DEFAULT_MIN_POOL_SIZE) <= (data.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE), {
|
|
2673
2693
|
message: "minPoolSize must be <= maxPoolSize"
|
|
@@ -2796,6 +2816,57 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
2796
2816
|
onAssociationComplete(listener) {
|
|
2797
2817
|
return this.on("ASSOCIATION_COMPLETE", listener);
|
|
2798
2818
|
}
|
|
2819
|
+
/**
|
|
2820
|
+
* Registers a listener for new associations (bubbled from workers).
|
|
2821
|
+
*
|
|
2822
|
+
* @param listener - Callback receiving association received data
|
|
2823
|
+
* @returns this for chaining
|
|
2824
|
+
*/
|
|
2825
|
+
onAssociationReceived(listener) {
|
|
2826
|
+
return this.on("ASSOCIATION_RECEIVED", listener);
|
|
2827
|
+
}
|
|
2828
|
+
/**
|
|
2829
|
+
* Registers a listener for C-STORE requests (bubbled from workers).
|
|
2830
|
+
*
|
|
2831
|
+
* @param listener - Callback receiving C-STORE request data
|
|
2832
|
+
* @returns this for chaining
|
|
2833
|
+
*/
|
|
2834
|
+
onCStoreRequest(listener) {
|
|
2835
|
+
return this.on("C_STORE_REQUEST", listener);
|
|
2836
|
+
}
|
|
2837
|
+
/**
|
|
2838
|
+
* Registers a listener for C-ECHO requests (bubbled from workers).
|
|
2839
|
+
*
|
|
2840
|
+
* @param listener - Callback receiving echo request data
|
|
2841
|
+
* @returns this for chaining
|
|
2842
|
+
*/
|
|
2843
|
+
onEchoRequest(listener) {
|
|
2844
|
+
return this.on("ECHO_REQUEST", listener);
|
|
2845
|
+
}
|
|
2846
|
+
/**
|
|
2847
|
+
* Registers a listener for refused associations (bubbled from workers).
|
|
2848
|
+
*
|
|
2849
|
+
* @param listener - Callback receiving refusing association data
|
|
2850
|
+
* @returns this for chaining
|
|
2851
|
+
*/
|
|
2852
|
+
onRefusingAssociation(listener) {
|
|
2853
|
+
return this.on("REFUSING_ASSOCIATION", listener);
|
|
2854
|
+
}
|
|
2855
|
+
/**
|
|
2856
|
+
* Routes an external socket to an idle worker.
|
|
2857
|
+
*
|
|
2858
|
+
* The pool must be started via `start()` before calling this method.
|
|
2859
|
+
* Use this when managing your own TCP listener (e.g., protocol router).
|
|
2860
|
+
*
|
|
2861
|
+
* @param socket - An incoming net.Socket to route to a worker
|
|
2862
|
+
*/
|
|
2863
|
+
handleSocket(socket) {
|
|
2864
|
+
if (!this.started) {
|
|
2865
|
+
socket.destroy(new Error("DicomReceiver: not started"));
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2868
|
+
void this.handleConnection(socket);
|
|
2869
|
+
}
|
|
2799
2870
|
/** Current pool status. */
|
|
2800
2871
|
get poolStatus() {
|
|
2801
2872
|
let idle = 0;
|
|
@@ -2809,8 +2880,11 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
2809
2880
|
// -----------------------------------------------------------------------
|
|
2810
2881
|
// TCP proxy
|
|
2811
2882
|
// -----------------------------------------------------------------------
|
|
2812
|
-
/** Starts the TCP proxy on the configured port. */
|
|
2883
|
+
/** Starts the TCP proxy on the configured port. Skips if port is 0. */
|
|
2813
2884
|
startTcpProxy() {
|
|
2885
|
+
if (this.options.port === 0) {
|
|
2886
|
+
return Promise.resolve(ok(void 0));
|
|
2887
|
+
}
|
|
2814
2888
|
return new Promise((resolve) => {
|
|
2815
2889
|
this.tcpServer = net.createServer((socket) => {
|
|
2816
2890
|
void this.handleConnection(socket);
|
|
@@ -2863,6 +2937,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
2863
2937
|
worker.associationDir = associationDir;
|
|
2864
2938
|
worker.files = [];
|
|
2865
2939
|
worker.fileSizes = [];
|
|
2940
|
+
worker.outputLines = [];
|
|
2866
2941
|
worker.startAt = Date.now();
|
|
2867
2942
|
this.pipeConnection(worker, remoteSocket);
|
|
2868
2943
|
void this.replenishPool();
|
|
@@ -2921,6 +2996,19 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
2921
2996
|
}
|
|
2922
2997
|
return ok(void 0);
|
|
2923
2998
|
}
|
|
2999
|
+
/** Creates a Dcmrecv instance with the pool's shared options. */
|
|
3000
|
+
createDcmrecv(port, tempDir) {
|
|
3001
|
+
return Dcmrecv.create({
|
|
3002
|
+
port,
|
|
3003
|
+
aeTitle: this.options.aeTitle ?? "DCMRECV",
|
|
3004
|
+
outputDirectory: tempDir,
|
|
3005
|
+
configFile: this.options.configFile,
|
|
3006
|
+
configProfile: this.options.configProfile,
|
|
3007
|
+
acseTimeout: this.options.acseTimeout,
|
|
3008
|
+
dimseTimeout: this.options.dimseTimeout,
|
|
3009
|
+
maxPdu: this.options.maxPdu
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
2924
3012
|
/** Spawns a single Dcmrecv worker with an ephemeral port. */
|
|
2925
3013
|
async spawnWorker() {
|
|
2926
3014
|
const portResult = await allocatePort();
|
|
@@ -2929,13 +3017,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
2929
3017
|
const tempDir = path.join(os.tmpdir(), `dcmrecv-pool-${String(port)}-${String(Date.now())}`);
|
|
2930
3018
|
const mkdirResult = await ensureDirectory(tempDir);
|
|
2931
3019
|
if (!mkdirResult.ok) return mkdirResult;
|
|
2932
|
-
const createResult =
|
|
2933
|
-
port,
|
|
2934
|
-
aeTitle: this.options.aeTitle ?? "DCMRECV",
|
|
2935
|
-
outputDirectory: tempDir,
|
|
2936
|
-
configFile: this.options.configFile,
|
|
2937
|
-
configProfile: this.options.configProfile
|
|
2938
|
-
});
|
|
3020
|
+
const createResult = this.createDcmrecv(port, tempDir);
|
|
2939
3021
|
if (!createResult.ok) return createResult;
|
|
2940
3022
|
const dcmrecv = createResult.value;
|
|
2941
3023
|
const worker = {
|
|
@@ -2947,6 +3029,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
2947
3029
|
associationDir: void 0,
|
|
2948
3030
|
files: [],
|
|
2949
3031
|
fileSizes: [],
|
|
3032
|
+
outputLines: [],
|
|
2950
3033
|
startAt: void 0,
|
|
2951
3034
|
remoteSocket: void 0,
|
|
2952
3035
|
workerSocket: void 0
|
|
@@ -3008,10 +3091,15 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3008
3091
|
// -----------------------------------------------------------------------
|
|
3009
3092
|
// Worker event wiring
|
|
3010
3093
|
// -----------------------------------------------------------------------
|
|
3011
|
-
/** Wires
|
|
3094
|
+
/** Wires all events on a worker: file handling, association lifecycle, output capture. */
|
|
3012
3095
|
wireWorkerEvents(worker) {
|
|
3013
3096
|
this.wireFileReceived(worker);
|
|
3014
3097
|
this.wireAssociationComplete(worker);
|
|
3098
|
+
this.wireAssociationReceived(worker);
|
|
3099
|
+
this.wireCStoreRequest(worker);
|
|
3100
|
+
this.wireEchoRequest(worker);
|
|
3101
|
+
this.wireRefusingAssociation(worker);
|
|
3102
|
+
this.wireOutputCapture(worker);
|
|
3015
3103
|
}
|
|
3016
3104
|
/** Wires FILE_RECEIVED from dcmrecv worker to handleFileReceived. */
|
|
3017
3105
|
wireFileReceived(worker) {
|
|
@@ -3059,6 +3147,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3059
3147
|
const assocId = worker.associationId ?? data.associationId;
|
|
3060
3148
|
const assocDir = worker.associationDir ?? "";
|
|
3061
3149
|
const files = [...worker.files];
|
|
3150
|
+
const output = [...worker.outputLines];
|
|
3062
3151
|
const endAt = Date.now();
|
|
3063
3152
|
const startAt = worker.startAt ?? endAt;
|
|
3064
3153
|
const totalBytes = sumArray(worker.fileSizes);
|
|
@@ -3076,19 +3165,65 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3076
3165
|
totalBytes,
|
|
3077
3166
|
bytesPerSecond,
|
|
3078
3167
|
startAt,
|
|
3079
|
-
endAt
|
|
3168
|
+
endAt,
|
|
3169
|
+
output
|
|
3080
3170
|
});
|
|
3081
3171
|
worker.state = "idle";
|
|
3082
3172
|
worker.associationId = void 0;
|
|
3083
3173
|
worker.associationDir = void 0;
|
|
3084
3174
|
worker.files = [];
|
|
3085
3175
|
worker.fileSizes = [];
|
|
3176
|
+
worker.outputLines = [];
|
|
3086
3177
|
worker.startAt = void 0;
|
|
3087
3178
|
worker.remoteSocket = void 0;
|
|
3088
3179
|
worker.workerSocket = void 0;
|
|
3089
3180
|
void this.scaleDown();
|
|
3090
3181
|
});
|
|
3091
3182
|
}
|
|
3183
|
+
/** Bubbles ASSOCIATION_RECEIVED from dcmrecv worker. */
|
|
3184
|
+
wireAssociationReceived(worker) {
|
|
3185
|
+
worker.dcmrecv.onEvent("ASSOCIATION_RECEIVED", (data) => {
|
|
3186
|
+
this.emit("ASSOCIATION_RECEIVED", {
|
|
3187
|
+
associationId: worker.associationId ?? "",
|
|
3188
|
+
callingAE: data.callingAE,
|
|
3189
|
+
calledAE: data.calledAE,
|
|
3190
|
+
source: data.source
|
|
3191
|
+
});
|
|
3192
|
+
});
|
|
3193
|
+
}
|
|
3194
|
+
/** Bubbles C_STORE_REQUEST from dcmrecv worker. */
|
|
3195
|
+
wireCStoreRequest(worker) {
|
|
3196
|
+
worker.dcmrecv.onEvent("C_STORE_REQUEST", (data) => {
|
|
3197
|
+
this.emit("C_STORE_REQUEST", {
|
|
3198
|
+
associationId: worker.associationId ?? "",
|
|
3199
|
+
raw: data.raw
|
|
3200
|
+
});
|
|
3201
|
+
});
|
|
3202
|
+
}
|
|
3203
|
+
/** Bubbles ECHO_REQUEST from dcmrecv worker. */
|
|
3204
|
+
wireEchoRequest(worker) {
|
|
3205
|
+
worker.dcmrecv.onEvent("ECHO_REQUEST", () => {
|
|
3206
|
+
this.emit("ECHO_REQUEST", {
|
|
3207
|
+
associationId: worker.associationId ?? ""
|
|
3208
|
+
});
|
|
3209
|
+
});
|
|
3210
|
+
}
|
|
3211
|
+
/** Bubbles REFUSING_ASSOCIATION from dcmrecv worker. */
|
|
3212
|
+
wireRefusingAssociation(worker) {
|
|
3213
|
+
worker.dcmrecv.onEvent("REFUSING_ASSOCIATION", (data) => {
|
|
3214
|
+
this.emit("REFUSING_ASSOCIATION", {
|
|
3215
|
+
reason: data.reason
|
|
3216
|
+
});
|
|
3217
|
+
});
|
|
3218
|
+
}
|
|
3219
|
+
/** Captures worker output lines during busy associations. */
|
|
3220
|
+
wireOutputCapture(worker) {
|
|
3221
|
+
worker.dcmrecv.on("line", ({ text }) => {
|
|
3222
|
+
if (worker.state === "busy" && worker.outputLines.length < MAX_OUTPUT_LINES_PER_ASSOCIATION) {
|
|
3223
|
+
worker.outputLines.push(text);
|
|
3224
|
+
}
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3092
3227
|
// -----------------------------------------------------------------------
|
|
3093
3228
|
// Abort signal
|
|
3094
3229
|
// -----------------------------------------------------------------------
|