@ubercode/dcmtk 0.12.0 → 0.13.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/servers.cjs CHANGED
@@ -2246,7 +2246,12 @@ var parser = new fastXmlParser.XMLParser({
2246
2246
  ignoreAttributes: false,
2247
2247
  attributeNamePrefix: "@_",
2248
2248
  parseTagValue: false,
2249
- isArray: (name) => ARRAY_TAG_NAMES.has(name)
2249
+ isArray: (name) => ARRAY_TAG_NAMES.has(name),
2250
+ // DICOM XML does not use XML entities. Disable entity processing to avoid
2251
+ // the default expansion limit (1000) which rejects files with >1000 tags.
2252
+ // Without this, large studies fall through to dcm2json which hangs on
2253
+ // compressed pixel data (DCMTK bug).
2254
+ processEntities: false
2250
2255
  });
2251
2256
  function xmlToJson(xml) {
2252
2257
  try {
@@ -2307,6 +2312,7 @@ var Dcm2jsonOptionsSchema = zod.z.object({
2307
2312
  signal: zod.z.instanceof(AbortSignal).optional(),
2308
2313
  directOnly: zod.z.boolean().optional(),
2309
2314
  charsetAssume: zod.z.string().min(1).optional(),
2315
+ charsetFallback: zod.z.string().min(1).optional(),
2310
2316
  verbosity: zod.z.enum(["verbose", "debug"]).optional()
2311
2317
  }).strict().optional();
2312
2318
  var VERBOSITY_FLAGS = { verbose: "-v", debug: "-d" };
@@ -2320,27 +2326,34 @@ function buildXmlOpts(options) {
2320
2326
  const result = {};
2321
2327
  if (options?.verbosity !== void 0) result["verbosity"] = options.verbosity;
2322
2328
  if (options?.charsetAssume !== void 0) result["charsetAssume"] = options.charsetAssume;
2329
+ if (options?.charsetFallback !== void 0) result["charsetFallback"] = options.charsetFallback;
2323
2330
  return result;
2324
2331
  }
2332
+ function isCharsetError(stderrOutput) {
2333
+ return stderrOutput.includes("convert character encoding") || stderrOutput.includes("Illegal byte sequence");
2334
+ }
2335
+ async function runXmlAndParse(binary, args, execOpts) {
2336
+ const xmlResult = await execCommand(binary, args, execOpts);
2337
+ if (!xmlResult.ok) return err(xmlResult.error);
2338
+ if (xmlResult.value.exitCode !== 0) {
2339
+ return err(createToolError("dcm2xml", args, xmlResult.value.exitCode, xmlResult.value.stderr));
2340
+ }
2341
+ const jsonResult = xmlToJson(xmlResult.value.stdout);
2342
+ if (!jsonResult.ok) return err(jsonResult.error);
2343
+ return ok({ data: jsonResult.value, source: "xml" });
2344
+ }
2325
2345
  async function tryXmlPath(inputPath, timeoutMs, signal, opts) {
2326
2346
  const xmlBinary = resolveBinary("dcm2xml");
2327
- if (!xmlBinary.ok) {
2328
- return err(xmlBinary.error);
2329
- }
2347
+ if (!xmlBinary.ok) return err(xmlBinary.error);
2330
2348
  const charsetArgs = opts?.charsetAssume !== void 0 ? ["+Ca", opts.charsetAssume] : [];
2331
2349
  const xmlArgs = [...buildVerbosityArgs(opts?.verbosity), ...charsetArgs, "-nat", inputPath];
2332
- const xmlResult = await execCommand(xmlBinary.value, xmlArgs, { timeoutMs, signal });
2333
- if (!xmlResult.ok) {
2334
- return err(xmlResult.error);
2350
+ const execOpts = signal !== void 0 ? { timeoutMs, signal } : { timeoutMs };
2351
+ const result = await runXmlAndParse(xmlBinary.value, xmlArgs, execOpts);
2352
+ if (!result.ok && opts?.charsetFallback !== void 0 && isCharsetError(result.error.message)) {
2353
+ const fallbackArgs = [...buildVerbosityArgs(opts.verbosity), "+Ca", opts.charsetFallback, "-nat", inputPath];
2354
+ return runXmlAndParse(xmlBinary.value, fallbackArgs, execOpts);
2335
2355
  }
2336
- if (xmlResult.value.exitCode !== 0) {
2337
- return err(createToolError("dcm2xml", xmlArgs, xmlResult.value.exitCode, xmlResult.value.stderr));
2338
- }
2339
- const jsonResult = xmlToJson(xmlResult.value.stdout);
2340
- if (!jsonResult.ok) {
2341
- return err(jsonResult.error);
2342
- }
2343
- return ok({ data: jsonResult.value, source: "xml" });
2356
+ return result;
2344
2357
  }
2345
2358
  async function tryDirectPath(inputPath, timeoutMs, signal, verbosity) {
2346
2359
  const jsonBinary = resolveBinary("dcm2json");
@@ -2525,7 +2538,8 @@ var DicomInstance = class _DicomInstance {
2525
2538
  const jsonResult = await dcm2json(path2, {
2526
2539
  timeoutMs: options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
2527
2540
  signal: options?.signal,
2528
- charsetAssume: options?.charsetAssume
2541
+ charsetAssume: options?.charsetAssume,
2542
+ charsetFallback: options?.charsetFallback
2529
2543
  });
2530
2544
  if (!jsonResult.ok) return err(jsonResult.error);
2531
2545
  const datasetResult = DicomDataset.fromJson(jsonResult.value.data);
@@ -2843,6 +2857,12 @@ var DicomReceiverOptionsSchema = zod.z.object({
2843
2857
  filenameMode: zod.z.enum(["default", "unique", "short-unique", "system-time"]).optional(),
2844
2858
  filenameExtension: zod.z.string().min(1).optional(),
2845
2859
  storageMode: zod.z.enum(["normal", "bit-preserving", "ignore"]).optional(),
2860
+ instanceOpenOptions: zod.z.object({
2861
+ timeoutMs: zod.z.number().int().positive().optional(),
2862
+ signal: zod.z.instanceof(AbortSignal).optional(),
2863
+ charsetAssume: zod.z.string().min(1).optional(),
2864
+ charsetFallback: zod.z.string().min(1).optional()
2865
+ }).strict().optional(),
2846
2866
  signal: zod.z.instanceof(AbortSignal).optional()
2847
2867
  }).strict().refine((data) => (data.minPoolSize ?? DEFAULT_MIN_POOL_SIZE) <= (data.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE), {
2848
2868
  message: "minPoolSize must be <= maxPoolSize"
@@ -3012,6 +3032,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3012
3032
  __publicField(this, "minPoolSize");
3013
3033
  __publicField(this, "maxPoolSize");
3014
3034
  __publicField(this, "connectionTimeoutMs");
3035
+ __publicField(this, "resolvedInstanceOpenOptions");
3015
3036
  __publicField(this, "workers", /* @__PURE__ */ new Map());
3016
3037
  __publicField(this, "tcpServer");
3017
3038
  __publicField(this, "associationCounter", 0);
@@ -3023,6 +3044,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3023
3044
  this.minPoolSize = options.minPoolSize ?? DEFAULT_MIN_POOL_SIZE;
3024
3045
  this.maxPoolSize = options.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE;
3025
3046
  this.connectionTimeoutMs = options.connectionTimeoutMs ?? DEFAULT_CONNECTION_TIMEOUT_MS;
3047
+ this.resolvedInstanceOpenOptions = { charsetFallback: "Latin1", ...options.instanceOpenOptions };
3026
3048
  }
3027
3049
  // -----------------------------------------------------------------------
3028
3050
  // Public API
@@ -3489,7 +3511,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3489
3511
  /** Parses a DICOM file and emits INSTANCE_RECEIVED or INSTANCE_ERROR. */
3490
3512
  async parseAndEmitInstance(worker, ctx) {
3491
3513
  try {
3492
- const openResult = await DicomInstance.open(ctx.filePath);
3514
+ const openResult = await DicomInstance.open(ctx.filePath, this.resolvedInstanceOpenOptions);
3493
3515
  if (!openResult.ok) {
3494
3516
  worker.recordInstanceError();
3495
3517
  this.emit("INSTANCE_ERROR", { error: openResult.error, thrown: false, ...ctx });