@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.
@@ -1,7 +1,7 @@
1
1
  import { L as LineSource, R as Result } from './types-Cgumy1N4.cjs';
2
2
  import { EventEmitter } from 'node:events';
3
3
  import * as net from 'node:net';
4
- import { d as DicomInstance } from './DicomInstance-CxLKC-oM.cjs';
4
+ import { d as DicomInstance, e as DicomOpenOptions } from './DicomInstance-BGXtON9T.cjs';
5
5
 
6
6
  /**
7
7
  * Base class for long-lived DCMTK processes (servers).
@@ -1622,6 +1622,8 @@ interface DicomReceiverOptions {
1622
1622
  readonly filenameExtension?: string | undefined;
1623
1623
  /** Storage mode for received files (passed through to Dcmrecv workers). */
1624
1624
  readonly storageMode?: StorageModeValue | undefined;
1625
+ /** Options passed to DicomInstance.open() for each received file. Defaults charsetFallback to `'Latin1'`. */
1626
+ readonly instanceOpenOptions?: DicomOpenOptions | undefined;
1625
1627
  /** AbortSignal for external cancellation. */
1626
1628
  readonly signal?: AbortSignal | undefined;
1627
1629
  }
@@ -1654,6 +1656,7 @@ declare class DicomReceiver extends EventEmitter<DicomReceiverEventMap> {
1654
1656
  private readonly minPoolSize;
1655
1657
  private readonly maxPoolSize;
1656
1658
  private readonly connectionTimeoutMs;
1659
+ private readonly resolvedInstanceOpenOptions;
1657
1660
  private readonly workers;
1658
1661
  private tcpServer;
1659
1662
  private associationCounter;
@@ -1,7 +1,7 @@
1
1
  import { L as LineSource, R as Result } from './types-Cgumy1N4.js';
2
2
  import { EventEmitter } from 'node:events';
3
3
  import * as net from 'node:net';
4
- import { d as DicomInstance } from './DicomInstance-BKUiir0G.js';
4
+ import { d as DicomInstance, e as DicomOpenOptions } from './DicomInstance-DGWB2mfD.js';
5
5
 
6
6
  /**
7
7
  * Base class for long-lived DCMTK processes (servers).
@@ -1622,6 +1622,8 @@ interface DicomReceiverOptions {
1622
1622
  readonly filenameExtension?: string | undefined;
1623
1623
  /** Storage mode for received files (passed through to Dcmrecv workers). */
1624
1624
  readonly storageMode?: StorageModeValue | undefined;
1625
+ /** Options passed to DicomInstance.open() for each received file. Defaults charsetFallback to `'Latin1'`. */
1626
+ readonly instanceOpenOptions?: DicomOpenOptions | undefined;
1625
1627
  /** AbortSignal for external cancellation. */
1626
1628
  readonly signal?: AbortSignal | undefined;
1627
1629
  }
@@ -1654,6 +1656,7 @@ declare class DicomReceiver extends EventEmitter<DicomReceiverEventMap> {
1654
1656
  private readonly minPoolSize;
1655
1657
  private readonly maxPoolSize;
1656
1658
  private readonly connectionTimeoutMs;
1659
+ private readonly resolvedInstanceOpenOptions;
1657
1660
  private readonly workers;
1658
1661
  private tcpServer;
1659
1662
  private associationCounter;
package/dist/index.cjs CHANGED
@@ -31402,7 +31402,12 @@ var parser = new fastXmlParser.XMLParser({
31402
31402
  ignoreAttributes: false,
31403
31403
  attributeNamePrefix: "@_",
31404
31404
  parseTagValue: false,
31405
- isArray: (name) => ARRAY_TAG_NAMES.has(name)
31405
+ isArray: (name) => ARRAY_TAG_NAMES.has(name),
31406
+ // DICOM XML does not use XML entities. Disable entity processing to avoid
31407
+ // the default expansion limit (1000) which rejects files with >1000 tags.
31408
+ // Without this, large studies fall through to dcm2json which hangs on
31409
+ // compressed pixel data (DCMTK bug).
31410
+ processEntities: false
31406
31411
  });
31407
31412
  function xmlToJson(xml) {
31408
31413
  try {
@@ -31463,6 +31468,7 @@ var Dcm2jsonOptionsSchema = zod.z.object({
31463
31468
  signal: zod.z.instanceof(AbortSignal).optional(),
31464
31469
  directOnly: zod.z.boolean().optional(),
31465
31470
  charsetAssume: zod.z.string().min(1).optional(),
31471
+ charsetFallback: zod.z.string().min(1).optional(),
31466
31472
  verbosity: zod.z.enum(["verbose", "debug"]).optional()
31467
31473
  }).strict().optional();
31468
31474
  var VERBOSITY_FLAGS2 = { verbose: "-v", debug: "-d" };
@@ -31476,27 +31482,34 @@ function buildXmlOpts(options) {
31476
31482
  const result = {};
31477
31483
  if (options?.verbosity !== void 0) result["verbosity"] = options.verbosity;
31478
31484
  if (options?.charsetAssume !== void 0) result["charsetAssume"] = options.charsetAssume;
31485
+ if (options?.charsetFallback !== void 0) result["charsetFallback"] = options.charsetFallback;
31479
31486
  return result;
31480
31487
  }
31488
+ function isCharsetError(stderrOutput) {
31489
+ return stderrOutput.includes("convert character encoding") || stderrOutput.includes("Illegal byte sequence");
31490
+ }
31491
+ async function runXmlAndParse(binary, args, execOpts) {
31492
+ const xmlResult = await execCommand(binary, args, execOpts);
31493
+ if (!xmlResult.ok) return err(xmlResult.error);
31494
+ if (xmlResult.value.exitCode !== 0) {
31495
+ return err(createToolError("dcm2xml", args, xmlResult.value.exitCode, xmlResult.value.stderr));
31496
+ }
31497
+ const jsonResult = xmlToJson(xmlResult.value.stdout);
31498
+ if (!jsonResult.ok) return err(jsonResult.error);
31499
+ return ok({ data: jsonResult.value, source: "xml" });
31500
+ }
31481
31501
  async function tryXmlPath(inputPath, timeoutMs, signal, opts) {
31482
31502
  const xmlBinary = resolveBinary("dcm2xml");
31483
- if (!xmlBinary.ok) {
31484
- return err(xmlBinary.error);
31485
- }
31503
+ if (!xmlBinary.ok) return err(xmlBinary.error);
31486
31504
  const charsetArgs = opts?.charsetAssume !== void 0 ? ["+Ca", opts.charsetAssume] : [];
31487
31505
  const xmlArgs = [...buildVerbosityArgs(opts?.verbosity), ...charsetArgs, "-nat", inputPath];
31488
- const xmlResult = await execCommand(xmlBinary.value, xmlArgs, { timeoutMs, signal });
31489
- if (!xmlResult.ok) {
31490
- return err(xmlResult.error);
31506
+ const execOpts = signal !== void 0 ? { timeoutMs, signal } : { timeoutMs };
31507
+ const result = await runXmlAndParse(xmlBinary.value, xmlArgs, execOpts);
31508
+ if (!result.ok && opts?.charsetFallback !== void 0 && isCharsetError(result.error.message)) {
31509
+ const fallbackArgs = [...buildVerbosityArgs(opts.verbosity), "+Ca", opts.charsetFallback, "-nat", inputPath];
31510
+ return runXmlAndParse(xmlBinary.value, fallbackArgs, execOpts);
31491
31511
  }
31492
- if (xmlResult.value.exitCode !== 0) {
31493
- return err(createToolError("dcm2xml", xmlArgs, xmlResult.value.exitCode, xmlResult.value.stderr));
31494
- }
31495
- const jsonResult = xmlToJson(xmlResult.value.stdout);
31496
- if (!jsonResult.ok) {
31497
- return err(jsonResult.error);
31498
- }
31499
- return ok({ data: jsonResult.value, source: "xml" });
31512
+ return result;
31500
31513
  }
31501
31514
  async function tryDirectPath(inputPath, timeoutMs, signal, verbosity) {
31502
31515
  const jsonBinary = resolveBinary("dcm2json");
@@ -34387,7 +34400,8 @@ var DicomInstance = class _DicomInstance {
34387
34400
  const jsonResult = await dcm2json(path2, {
34388
34401
  timeoutMs: options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
34389
34402
  signal: options?.signal,
34390
- charsetAssume: options?.charsetAssume
34403
+ charsetAssume: options?.charsetAssume,
34404
+ charsetFallback: options?.charsetFallback
34391
34405
  });
34392
34406
  if (!jsonResult.ok) return err(jsonResult.error);
34393
34407
  const datasetResult = DicomDataset.fromJson(jsonResult.value.data);
@@ -36472,6 +36486,12 @@ var DicomReceiverOptionsSchema = zod.z.object({
36472
36486
  filenameMode: zod.z.enum(["default", "unique", "short-unique", "system-time"]).optional(),
36473
36487
  filenameExtension: zod.z.string().min(1).optional(),
36474
36488
  storageMode: zod.z.enum(["normal", "bit-preserving", "ignore"]).optional(),
36489
+ instanceOpenOptions: zod.z.object({
36490
+ timeoutMs: zod.z.number().int().positive().optional(),
36491
+ signal: zod.z.instanceof(AbortSignal).optional(),
36492
+ charsetAssume: zod.z.string().min(1).optional(),
36493
+ charsetFallback: zod.z.string().min(1).optional()
36494
+ }).strict().optional(),
36475
36495
  signal: zod.z.instanceof(AbortSignal).optional()
36476
36496
  }).strict().refine((data) => (data.minPoolSize ?? DEFAULT_MIN_POOL_SIZE) <= (data.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE), {
36477
36497
  message: "minPoolSize must be <= maxPoolSize"
@@ -36641,6 +36661,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36641
36661
  __publicField(this, "minPoolSize");
36642
36662
  __publicField(this, "maxPoolSize");
36643
36663
  __publicField(this, "connectionTimeoutMs");
36664
+ __publicField(this, "resolvedInstanceOpenOptions");
36644
36665
  __publicField(this, "workers", /* @__PURE__ */ new Map());
36645
36666
  __publicField(this, "tcpServer");
36646
36667
  __publicField(this, "associationCounter", 0);
@@ -36652,6 +36673,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36652
36673
  this.minPoolSize = options.minPoolSize ?? DEFAULT_MIN_POOL_SIZE;
36653
36674
  this.maxPoolSize = options.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE;
36654
36675
  this.connectionTimeoutMs = options.connectionTimeoutMs ?? DEFAULT_CONNECTION_TIMEOUT_MS;
36676
+ this.resolvedInstanceOpenOptions = { charsetFallback: "Latin1", ...options.instanceOpenOptions };
36655
36677
  }
36656
36678
  // -----------------------------------------------------------------------
36657
36679
  // Public API
@@ -37118,7 +37140,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
37118
37140
  /** Parses a DICOM file and emits INSTANCE_RECEIVED or INSTANCE_ERROR. */
37119
37141
  async parseAndEmitInstance(worker, ctx) {
37120
37142
  try {
37121
- const openResult = await DicomInstance.open(ctx.filePath);
37143
+ const openResult = await DicomInstance.open(ctx.filePath, this.resolvedInstanceOpenOptions);
37122
37144
  if (!openResult.ok) {
37123
37145
  worker.recordInstanceError();
37124
37146
  this.emit("INSTANCE_ERROR", { error: openResult.error, thrown: false, ...ctx });