@ubercode/dcmtk 1.0.0-rc.1 → 1.0.0-rc.3

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/index.js CHANGED
@@ -6,10 +6,12 @@ import { spawn, execSync } from 'child_process';
6
6
  import kill from 'tree-kill';
7
7
  import { EventEmitter } from 'events';
8
8
  import { stderr, tryCatch } from 'stderr-lib';
9
- import { mkdir, stat, rm, copyFile, unlink, rename, mkdtemp, readdir } from 'fs/promises';
9
+ import { mkdir, stat, rm, readFile, copyFile, unlink, rename, mkdtemp, open, readdir } from 'fs/promises';
10
10
  import * as os from 'os';
11
11
  import { tmpdir } from 'os';
12
12
  import { XMLParser } from 'fast-xml-parser';
13
+ import { inflateRawSync } from 'zlib';
14
+ import dicomParser2 from 'dicom-parser';
13
15
  import * as net from 'net';
14
16
  import { setTimeout as setTimeout$1 } from 'timers/promises';
15
17
  import { randomUUID } from 'crypto';
@@ -192,6 +194,8 @@ var MAX_EVENT_PATTERNS = 200;
192
194
  var MAX_TRAVERSAL_DEPTH = 50;
193
195
  var MAX_CHANGESET_OPERATIONS = 1e4;
194
196
  var MAX_OUTPUT_BYTES = 100 * 1024 * 1024;
197
+ var BOUNDED_READ_THRESHOLD_BYTES = 8 * 1024 * 1024;
198
+ var BOUNDED_READ_CHUNK_BYTES = 1024 * 1024;
195
199
  var cachedPath;
196
200
  var isWindows = process.platform === "win32";
197
201
  function binaryName(name) {
@@ -298,7 +302,7 @@ async function execCommand(binary, args, options) {
298
302
  }
299
303
  function wireSpawnListeners(binary, child, timeoutMs, resolve) {
300
304
  let stdout = "";
301
- let stderr8 = "";
305
+ let stderr9 = "";
302
306
  let settled = false;
303
307
  const name = binary.split("/").pop() ?? binary;
304
308
  const settle = (result) => {
@@ -313,14 +317,14 @@ function wireSpawnListeners(binary, child, timeoutMs, resolve) {
313
317
  }, timeoutMs);
314
318
  child.stdout?.on("data", (chunk) => {
315
319
  stdout += String(chunk);
316
- if (stdout.length + stderr8.length > MAX_OUTPUT_BYTES) {
320
+ if (stdout.length + stderr9.length > MAX_OUTPUT_BYTES) {
317
321
  if (child.pid !== void 0 && child.pid !== null) killTree(child.pid);
318
322
  settle(err(new Error(`${name} output exceeded ${MAX_OUTPUT_BYTES} bytes`)));
319
323
  }
320
324
  });
321
325
  child.stderr?.on("data", (chunk) => {
322
- stderr8 += String(chunk);
323
- if (stdout.length + stderr8.length > MAX_OUTPUT_BYTES) {
326
+ stderr9 += String(chunk);
327
+ if (stdout.length + stderr9.length > MAX_OUTPUT_BYTES) {
324
328
  if (child.pid !== void 0 && child.pid !== null) killTree(child.pid);
325
329
  settle(err(new Error(`${name} output exceeded ${MAX_OUTPUT_BYTES} bytes`)));
326
330
  }
@@ -330,7 +334,7 @@ function wireSpawnListeners(binary, child, timeoutMs, resolve) {
330
334
  settle(err(new Error(`${name} error: ${error.message}`)));
331
335
  });
332
336
  child.on("close", (code) => {
333
- settle(ok({ stdout, stderr: stderr8, exitCode: code ?? 1 }));
337
+ settle(ok({ stdout, stderr: stderr9, exitCode: code ?? 1 }));
334
338
  });
335
339
  }
336
340
  async function spawnCommand(binary, args, options) {
@@ -709,9 +713,9 @@ var ToolExecutionError = class extends Error {
709
713
  this.exitCode = details.exitCode;
710
714
  }
711
715
  };
712
- function createToolError(toolName, args, exitCode, stderr8, stdout = "") {
716
+ function createToolError(toolName, args, exitCode, stderr9, stdout = "") {
713
717
  const argsStr = truncate(args.join(" "), MAX_ARGS_LENGTH);
714
- const stderrStr = truncate(stderr8.trim(), MAX_STDERR_LENGTH);
718
+ const stderrStr = truncate(stderr9.trim(), MAX_STDERR_LENGTH);
715
719
  const parts = [`${toolName} failed (exit code ${String(exitCode)})`];
716
720
  if (argsStr.length > 0) {
717
721
  parts.push(`args: ${argsStr}`);
@@ -719,7 +723,7 @@ function createToolError(toolName, args, exitCode, stderr8, stdout = "") {
719
723
  if (stderrStr.length > 0) {
720
724
  parts.push(`stderr: ${stderrStr}`);
721
725
  }
722
- return new ToolExecutionError(parts.join(" | "), { stdout, stderr: stderr8, exitCode });
726
+ return new ToolExecutionError(parts.join(" | "), { stdout, stderr: stderr9, exitCode });
723
727
  }
724
728
  function createValidationError(toolName, zodError) {
725
729
  const parts = [];
@@ -31339,8 +31343,8 @@ function unwrapValue(v) {
31339
31343
  const obj = v;
31340
31344
  if ("#text" in obj) return obj["#text"];
31341
31345
  const keys = Object.keys(obj);
31342
- if (keys.length === 1 && keys[0] !== void 0 && keys[0].startsWith("@_")) {
31343
- return obj[keys[0]];
31346
+ if (keys.every((k) => k.startsWith("@_"))) {
31347
+ return "";
31344
31348
  }
31345
31349
  return v;
31346
31350
  }
@@ -31546,16 +31550,959 @@ async function dcm2json(inputPath, options) {
31546
31550
  const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
31547
31551
  const signal = options?.signal;
31548
31552
  const startTime = Date.now();
31549
- const remaining = () => Math.max(1e3, timeoutMs - (Date.now() - startTime));
31553
+ const remaining = () => Math.max(0, timeoutMs - (Date.now() - startTime));
31550
31554
  const verbosity = options?.verbosity;
31551
31555
  if (options?.directOnly === true) {
31552
- return tryDirectPath(inputPath, remaining(), signal, verbosity);
31556
+ return tryDirectPath(inputPath, timeoutMs, signal, verbosity);
31553
31557
  }
31554
- const xmlResult = await tryXmlPath(inputPath, remaining(), signal, buildXmlOpts(options));
31558
+ const xmlResult = await tryXmlPath(inputPath, timeoutMs, signal, buildXmlOpts(options));
31555
31559
  if (xmlResult.ok) {
31556
31560
  return xmlResult;
31557
31561
  }
31558
- return tryDirectPath(inputPath, remaining(), signal, verbosity);
31562
+ return fallbackToDirect(inputPath, xmlResult.error, { budget: remaining(), timeoutMs, signal, verbosity });
31563
+ }
31564
+ async function fallbackToDirect(inputPath, xmlError, context) {
31565
+ if (context.budget === 0) {
31566
+ return err(
31567
+ new Error(
31568
+ `dcm2json: XML path failed and timeout budget (${String(context.timeoutMs)}ms) is exhausted (skipping direct fallback) | xml: ${xmlError.message}`
31569
+ )
31570
+ );
31571
+ }
31572
+ const directResult = await tryDirectPath(inputPath, context.budget, context.signal, context.verbosity);
31573
+ if (directResult.ok) {
31574
+ return directResult;
31575
+ }
31576
+ return err(new Error(`dcm2json: both paths failed | xml: ${xmlError.message} | direct: ${directResult.error.message}`));
31577
+ }
31578
+
31579
+ // src/tools/_charset.ts
31580
+ var LATIN1 = { kind: "latin1" };
31581
+ function label(name) {
31582
+ return { kind: "label", label: name };
31583
+ }
31584
+ var CHARSET_DECODERS = {
31585
+ "ISO_IR 6": LATIN1,
31586
+ "ISO_IR 100": LATIN1,
31587
+ "ISO_IR 101": label("iso-8859-2"),
31588
+ "ISO_IR 109": label("iso-8859-3"),
31589
+ "ISO_IR 110": label("iso-8859-4"),
31590
+ "ISO_IR 144": label("iso-8859-5"),
31591
+ "ISO_IR 127": label("iso-8859-6"),
31592
+ "ISO_IR 126": label("iso-8859-7"),
31593
+ "ISO_IR 138": label("iso-8859-8"),
31594
+ "ISO_IR 148": label("iso-8859-9"),
31595
+ "ISO_IR 203": label("iso-8859-15"),
31596
+ "ISO_IR 13": label("shift_jis"),
31597
+ "ISO_IR 166": label("windows-874"),
31598
+ "ISO_IR 192": label("utf-8"),
31599
+ GB18030: label("gb18030"),
31600
+ GBK: label("gbk")
31601
+ };
31602
+ var ISO2022_INITIAL = {
31603
+ "ISO 2022 IR 6": LATIN1,
31604
+ "ISO 2022 IR 13": label("shift_jis"),
31605
+ "ISO 2022 IR 87": LATIN1,
31606
+ "ISO 2022 IR 159": LATIN1,
31607
+ "ISO 2022 IR 149": label("euc-kr"),
31608
+ "ISO 2022 IR 58": label("gbk"),
31609
+ "ISO 2022 IR 100": LATIN1,
31610
+ "ISO 2022 IR 101": label("iso-8859-2"),
31611
+ "ISO 2022 IR 109": label("iso-8859-3"),
31612
+ "ISO 2022 IR 110": label("iso-8859-4"),
31613
+ "ISO 2022 IR 144": label("iso-8859-5"),
31614
+ "ISO 2022 IR 127": label("iso-8859-6"),
31615
+ "ISO 2022 IR 126": label("iso-8859-7"),
31616
+ "ISO 2022 IR 138": label("iso-8859-8"),
31617
+ "ISO 2022 IR 148": label("iso-8859-9"),
31618
+ "ISO 2022 IR 166": label("windows-874"),
31619
+ "ISO 2022 IR 203": label("iso-8859-15")
31620
+ };
31621
+ var ESCAPE_DECODERS = {
31622
+ "(B": LATIN1,
31623
+ // G0 = ASCII
31624
+ "(J": LATIN1,
31625
+ // G0 = JIS X 0201 romaji
31626
+ ")I": { kind: "katakana" },
31627
+ // G1 = JIS X 0201 katakana
31628
+ "$@": { kind: "iso2022jp", escape: [27, 36, 64] },
31629
+ // G0 = JIS X 0208-1978
31630
+ $B: { kind: "iso2022jp", escape: [27, 36, 66] },
31631
+ // G0 = JIS X 0208
31632
+ "$(D": { kind: "iso2022jp", escape: [27, 36, 40, 68] },
31633
+ // G0 = JIS X 0212
31634
+ "$)C": label("euc-kr"),
31635
+ // G1 = KS X 1001
31636
+ "$)A": label("gbk"),
31637
+ // G1 = GB 2312
31638
+ "-A": LATIN1,
31639
+ // G1 = ISO-8859-1
31640
+ "-B": label("iso-8859-2"),
31641
+ "-C": label("iso-8859-3"),
31642
+ "-D": label("iso-8859-4"),
31643
+ "-F": label("iso-8859-7"),
31644
+ "-G": label("iso-8859-6"),
31645
+ "-H": label("iso-8859-8"),
31646
+ "-L": label("iso-8859-5"),
31647
+ "-M": label("iso-8859-9"),
31648
+ "-T": label("windows-874"),
31649
+ "-b": label("iso-8859-15")
31650
+ };
31651
+ var CHARSET_ALIASES = {
31652
+ ascii: "ISO_IR 6",
31653
+ "latin-1": "ISO_IR 100",
31654
+ latin1: "ISO_IR 100",
31655
+ "iso-8859-1": "ISO_IR 100",
31656
+ "latin-2": "ISO_IR 101",
31657
+ "latin-3": "ISO_IR 109",
31658
+ "latin-4": "ISO_IR 110",
31659
+ "latin-5": "ISO_IR 148",
31660
+ "latin-9": "ISO_IR 203",
31661
+ cyrillic: "ISO_IR 144",
31662
+ arabic: "ISO_IR 127",
31663
+ greek: "ISO_IR 126",
31664
+ hebrew: "ISO_IR 138",
31665
+ thai: "ISO_IR 166",
31666
+ "shift-jis": "ISO_IR 13",
31667
+ shift_jis: "ISO_IR 13",
31668
+ "utf-8": "ISO_IR 192",
31669
+ utf8: "ISO_IR 192",
31670
+ gb18030: "GB18030",
31671
+ gbk: "GBK"
31672
+ };
31673
+ var SINGLE_BYTE_LABELS = /* @__PURE__ */ new Set([
31674
+ "iso-8859-2",
31675
+ "iso-8859-3",
31676
+ "iso-8859-4",
31677
+ "iso-8859-5",
31678
+ "iso-8859-6",
31679
+ "iso-8859-7",
31680
+ "iso-8859-8",
31681
+ "iso-8859-9",
31682
+ "iso-8859-15",
31683
+ "windows-874"
31684
+ ]);
31685
+ function isSingleByteDecoder(decoder) {
31686
+ return decoder.kind === "latin1" || decoder.kind === "label" && SINGLE_BYTE_LABELS.has(decoder.label);
31687
+ }
31688
+ var decoderCache = /* @__PURE__ */ new Map();
31689
+ function getTextDecoder(labelName) {
31690
+ if (decoderCache.has(labelName)) {
31691
+ return decoderCache.get(labelName);
31692
+ }
31693
+ let decoder;
31694
+ try {
31695
+ decoder = new TextDecoder(labelName);
31696
+ } catch {
31697
+ decoder = void 0;
31698
+ }
31699
+ decoderCache.set(labelName, decoder);
31700
+ return decoder;
31701
+ }
31702
+ function decodeLatin1(bytes) {
31703
+ return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("latin1");
31704
+ }
31705
+ function decodeSegment(bytes, decoder) {
31706
+ if (bytes.length === 0) return "";
31707
+ switch (decoder.kind) {
31708
+ case "latin1":
31709
+ return decodeLatin1(bytes);
31710
+ case "label": {
31711
+ const td = getTextDecoder(decoder.label);
31712
+ if (td === void 0) return decodeLatin1(bytes);
31713
+ return td.decode(bytes);
31714
+ }
31715
+ case "iso2022jp": {
31716
+ const td = getTextDecoder("iso-2022-jp");
31717
+ if (td === void 0) return decodeLatin1(bytes);
31718
+ const wrapped = new Uint8Array(decoder.escape.length + bytes.length);
31719
+ wrapped.set(decoder.escape, 0);
31720
+ wrapped.set(bytes, decoder.escape.length);
31721
+ return td.decode(wrapped);
31722
+ }
31723
+ case "katakana": {
31724
+ const td = getTextDecoder("shift_jis");
31725
+ if (td === void 0) return decodeLatin1(bytes);
31726
+ return td.decode(bytes);
31727
+ }
31728
+ }
31729
+ }
31730
+ function readEscape(bytes, start) {
31731
+ let i = start + 1;
31732
+ while (i < bytes.length && bytes[i] >= 32 && bytes[i] <= 47) {
31733
+ i++;
31734
+ }
31735
+ if (i < bytes.length && bytes[i] >= 48 && bytes[i] <= 126) {
31736
+ i++;
31737
+ }
31738
+ const seq = bytes.subarray(start + 1, i);
31739
+ return { key: decodeLatin1(seq), length: i - start };
31740
+ }
31741
+ function decodeIso2022(bytes, initial) {
31742
+ let out = "";
31743
+ let current = initial;
31744
+ let segStart = 0;
31745
+ let i = 0;
31746
+ while (i < bytes.length) {
31747
+ if (bytes[i] !== 27) {
31748
+ i++;
31749
+ continue;
31750
+ }
31751
+ out += decodeSegment(bytes.subarray(segStart, i), current);
31752
+ const esc = readEscape(bytes, i);
31753
+ current = ESCAPE_DECODERS[esc.key] ?? current;
31754
+ i += esc.length;
31755
+ segStart = i;
31756
+ }
31757
+ out += decodeSegment(bytes.subarray(segStart), current);
31758
+ return out;
31759
+ }
31760
+ var DEFAULT_CONTEXT = { terms: ["ISO_IR 6"], iso2022: false, initial: LATIN1, singleByte: true };
31761
+ function normalizeCharsetName(input) {
31762
+ const trimmed = input.trim();
31763
+ if (CHARSET_DECODERS[trimmed] !== void 0 || ISO2022_INITIAL[trimmed] !== void 0) {
31764
+ return trimmed;
31765
+ }
31766
+ return CHARSET_ALIASES[trimmed.toLowerCase()];
31767
+ }
31768
+ function buildContext(terms) {
31769
+ const iso2022 = terms.length > 1 || terms.some((t) => t.startsWith("ISO 2022"));
31770
+ const first = terms[0] ?? "ISO_IR 6";
31771
+ const initial = iso2022 ? ISO2022_INITIAL[first] : CHARSET_DECODERS[first];
31772
+ if (initial === void 0) return void 0;
31773
+ return { terms, iso2022, initial, singleByte: !iso2022 && isSingleByteDecoder(initial) };
31774
+ }
31775
+ function parseSpecificCharacterSet(raw) {
31776
+ const values = raw.split("\\").map((v) => v.trim());
31777
+ return values.map((v, idx) => v === "" && idx === 0 ? "ISO 2022 IR 6" : v).filter((v) => v !== "");
31778
+ }
31779
+ function contextFromTerms(terms, fallback, source) {
31780
+ const context = buildContext(terms);
31781
+ if (context !== void 0) {
31782
+ return ok(context);
31783
+ }
31784
+ if (fallback !== void 0) {
31785
+ const fallbackTerm = normalizeCharsetName(fallback);
31786
+ const fallbackContext = fallbackTerm !== void 0 ? buildContext([fallbackTerm]) : void 0;
31787
+ if (fallbackContext !== void 0) {
31788
+ return ok(fallbackContext);
31789
+ }
31790
+ return err(new Error(`Unsupported charset fallback '${fallback}' (while handling unsupported ${source})`));
31791
+ }
31792
+ return err(new Error(`Unsupported character set: ${source} \u2014 provide charsetFallback (e.g. 'latin-1') to decode best-effort`));
31793
+ }
31794
+ function resolveCharsetContext(specificCharacterSet, charsetAssume, charsetFallback) {
31795
+ if (specificCharacterSet === void 0 || specificCharacterSet.trim() === "") {
31796
+ if (charsetAssume === void 0) {
31797
+ return ok(DEFAULT_CONTEXT);
31798
+ }
31799
+ const term = normalizeCharsetName(charsetAssume);
31800
+ if (term === void 0) {
31801
+ return contextFromTerms([charsetAssume], charsetFallback, `charsetAssume '${charsetAssume}'`);
31802
+ }
31803
+ return contextFromTerms([term], charsetFallback, `charsetAssume '${charsetAssume}'`);
31804
+ }
31805
+ const terms = parseSpecificCharacterSet(specificCharacterSet);
31806
+ return contextFromTerms(terms, charsetFallback, `'${specificCharacterSet}' (0008,0005)`);
31807
+ }
31808
+ function decodeDicomText(bytes, context) {
31809
+ if (context.iso2022) {
31810
+ return decodeIso2022(bytes, context.initial);
31811
+ }
31812
+ return decodeSegment(bytes, context.initial);
31813
+ }
31814
+ var strictUtf8;
31815
+ function isProbableUtf8Mislabel(bytes) {
31816
+ let hasHighByte = false;
31817
+ for (const byte of bytes) {
31818
+ if (byte >= 128) {
31819
+ hasHighByte = true;
31820
+ break;
31821
+ }
31822
+ }
31823
+ if (!hasHighByte) return false;
31824
+ strictUtf8 ?? (strictUtf8 = new TextDecoder("utf-8", { fatal: true }));
31825
+ try {
31826
+ strictUtf8.decode(bytes);
31827
+ return true;
31828
+ } catch {
31829
+ return false;
31830
+ }
31831
+ }
31832
+ var UTF8_DECODER = label("utf-8");
31833
+ function decodeUtf8(bytes) {
31834
+ return decodeSegment(bytes, UTF8_DECODER);
31835
+ }
31836
+
31837
+ // src/tools/_p10ToJson.ts
31838
+ var BINARY_VRS = /* @__PURE__ */ new Set(["OB", "OW", "OD", "OF", "OL", "OV", "UN"]);
31839
+ var CHARSET_VRS = /* @__PURE__ */ new Set(["SH", "LO", "ST", "LT", "UC", "UT", "PN"]);
31840
+ var NO_SPLIT_VRS = /* @__PURE__ */ new Set(["ST", "LT", "UT", "UR"]);
31841
+ var NUMERIC_VR_SIZE = { US: 2, SS: 2, UL: 4, SL: 4, FL: 4, FD: 8, SV: 8, UV: 8 };
31842
+ var MAX_DATASETS = 1e5;
31843
+ function toJsonTag(tag2) {
31844
+ return tag2.slice(1).toUpperCase();
31845
+ }
31846
+ function isEncodingArtifact(tag2) {
31847
+ return tag2.endsWith("0000") && tag2.length === 9 || tag2.startsWith("xfffe");
31848
+ }
31849
+ function resolveVr(element) {
31850
+ const vr = element.vr;
31851
+ if (vr !== void 0 && KNOWN_VR_CODES.has(vr)) {
31852
+ return vr;
31853
+ }
31854
+ return "UN";
31855
+ }
31856
+ function valueBytes(element, byteArray) {
31857
+ return byteArray.subarray(element.dataOffset, element.dataOffset + element.length);
31858
+ }
31859
+ function trimPadding(value) {
31860
+ let end = value.length;
31861
+ while (end > 0) {
31862
+ const ch = value.charCodeAt(end - 1);
31863
+ if (ch !== 32 && ch !== 0) break;
31864
+ end--;
31865
+ }
31866
+ return value.slice(0, end);
31867
+ }
31868
+ function readNumericAt(view, offset, vr, littleEndian) {
31869
+ switch (vr) {
31870
+ case "US":
31871
+ return view.getUint16(offset, littleEndian);
31872
+ case "SS":
31873
+ return view.getInt16(offset, littleEndian);
31874
+ case "UL":
31875
+ return view.getUint32(offset, littleEndian);
31876
+ case "SL":
31877
+ return view.getInt32(offset, littleEndian);
31878
+ case "FL":
31879
+ return view.getFloat32(offset, littleEndian);
31880
+ case "FD":
31881
+ return view.getFloat64(offset, littleEndian);
31882
+ case "SV":
31883
+ return Number(view.getBigInt64(offset, littleEndian));
31884
+ /* v8 ignore next 2 -- 'UV' is the only remaining caller-provided VR */
31885
+ default:
31886
+ return Number(view.getBigUint64(offset, littleEndian));
31887
+ }
31888
+ }
31889
+ function convertNumeric(element, byteArray, vr, littleEndian) {
31890
+ const size = NUMERIC_VR_SIZE[vr];
31891
+ const count = Math.floor(element.length / size);
31892
+ if (count === 0) {
31893
+ return { vr };
31894
+ }
31895
+ const view = new DataView(byteArray.buffer, byteArray.byteOffset + element.dataOffset, element.length);
31896
+ const values = [];
31897
+ for (let i = 0; i < count; i++) {
31898
+ values.push(readNumericAt(view, i * size, vr, littleEndian));
31899
+ }
31900
+ return { vr, Value: values };
31901
+ }
31902
+ function convertAttributeTag(element, byteArray, littleEndian) {
31903
+ const count = Math.floor(element.length / 4);
31904
+ if (count === 0) {
31905
+ return { vr: "AT" };
31906
+ }
31907
+ const view = new DataView(byteArray.buffer, byteArray.byteOffset + element.dataOffset, element.length);
31908
+ const values = [];
31909
+ for (let i = 0; i < count; i++) {
31910
+ const group = view.getUint16(i * 4, littleEndian);
31911
+ const elem = view.getUint16(i * 4 + 2, littleEndian);
31912
+ values.push(group.toString(16).padStart(4, "0").toUpperCase() + elem.toString(16).padStart(4, "0").toUpperCase());
31913
+ }
31914
+ return { vr: "AT", Value: values };
31915
+ }
31916
+ function personNameToJson(value) {
31917
+ const groups = trimPadding(value).split("=");
31918
+ const result = {};
31919
+ const names = ["Alphabetic", "Ideographic", "Phonetic"];
31920
+ for (let i = 0; i < names.length; i++) {
31921
+ const group = (groups[i] ?? "").replace(/\^+$/u, "");
31922
+ const name = names[i];
31923
+ if (group !== "" && name !== void 0) {
31924
+ result[name] = group;
31925
+ }
31926
+ }
31927
+ return result;
31928
+ }
31929
+ function decodeTextChecked(bytes, tag2, settings) {
31930
+ const { charset, mislabel } = settings;
31931
+ if (charset.singleByte && isProbableUtf8Mislabel(bytes)) {
31932
+ if (!mislabel.seen.has(tag2)) {
31933
+ mislabel.seen.add(tag2);
31934
+ const action = mislabel.promote ? "decoded as UTF-8" : `decoded as '${charset.terms.join("\\")}'`;
31935
+ mislabel.warnings.push(`possible UTF-8 mislabel: ${tag2} (${action})`);
31936
+ }
31937
+ if (mislabel.promote) {
31938
+ return decodeUtf8(bytes);
31939
+ }
31940
+ }
31941
+ return decodeDicomText(bytes, charset);
31942
+ }
31943
+ function convertPersonName2(element, byteArray, settings) {
31944
+ const decoded = decodeTextChecked(valueBytes(element, byteArray), toJsonTag(element.tag), settings);
31945
+ const values = decoded.split("\\").map(personNameToJson);
31946
+ return { vr: "PN", Value: values };
31947
+ }
31948
+ function convertString(element, byteArray, vr, settings) {
31949
+ const bytes = valueBytes(element, byteArray);
31950
+ const decoded = CHARSET_VRS.has(vr) ? decodeTextChecked(bytes, toJsonTag(element.tag), settings) : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("latin1");
31951
+ const rawValues = NO_SPLIT_VRS.has(vr) ? [decoded] : decoded.split("\\");
31952
+ const values = rawValues.map((v) => {
31953
+ const trimmed = trimPadding(v);
31954
+ return vr === "DS" || vr === "IS" ? coerceNumeric(trimmed.trim()) : trimmed;
31955
+ });
31956
+ if (values.length === 1 && values[0] === "") {
31957
+ return { vr };
31958
+ }
31959
+ return { vr, Value: values };
31960
+ }
31961
+ function convertLeaf(element, byteArray, vr, settings) {
31962
+ if (element.fragments !== void 0) {
31963
+ return { vr: "OB" };
31964
+ }
31965
+ if (BINARY_VRS.has(vr)) {
31966
+ return { vr };
31967
+ }
31968
+ if (element.length === 0) {
31969
+ return { vr };
31970
+ }
31971
+ if (NUMERIC_VR_SIZE[vr] !== void 0) {
31972
+ return convertNumeric(element, byteArray, vr, settings.littleEndian);
31973
+ }
31974
+ if (vr === "AT") {
31975
+ return convertAttributeTag(element, byteArray, settings.littleEndian);
31976
+ }
31977
+ if (vr === "PN") {
31978
+ return convertPersonName2(element, byteArray, settings);
31979
+ }
31980
+ return convertString(element, byteArray, vr, settings);
31981
+ }
31982
+ function datasetCharset(src, parent, options) {
31983
+ const raw = src.string("x00080005");
31984
+ if (raw === void 0) {
31985
+ return ok(parent);
31986
+ }
31987
+ return resolveCharsetContext(raw, void 0, options?.charsetFallback);
31988
+ }
31989
+ function convertSequence2(element, stack, charset) {
31990
+ const items = element.items ?? [];
31991
+ const values = [];
31992
+ for (const item of items) {
31993
+ const model = {};
31994
+ values.push(model);
31995
+ if (item.dataSet !== void 0) {
31996
+ stack.push({ src: item.dataSet, out: model, parentCharset: charset });
31997
+ }
31998
+ }
31999
+ if (values.length === 0) {
32000
+ return { vr: "SQ" };
32001
+ }
32002
+ return { vr: "SQ", Value: values };
32003
+ }
32004
+ function isSequence(element, vr) {
32005
+ if (element.fragments !== void 0) {
32006
+ return false;
32007
+ }
32008
+ return vr === "SQ" || element.vr === void 0 && element.items !== void 0;
32009
+ }
32010
+ function convertDataset(unit, stack, state) {
32011
+ const charsetResult = datasetCharset(unit.src, unit.parentCharset, state.options);
32012
+ if (!charsetResult.ok) {
32013
+ return err(charsetResult.error);
32014
+ }
32015
+ const charset = charsetResult.value;
32016
+ const keys = Object.keys(unit.src.elements).sort();
32017
+ for (const key of keys) {
32018
+ const element = unit.src.elements[key];
32019
+ if (element === void 0) continue;
32020
+ if (isEncodingArtifact(element.tag)) continue;
32021
+ const tag2 = toJsonTag(element.tag);
32022
+ const vr = resolveVr(element);
32023
+ if (isSequence(element, vr)) {
32024
+ unit.out[tag2] = convertSequence2(element, stack, charset);
32025
+ } else {
32026
+ unit.out[tag2] = convertLeaf(element, unit.src.byteArray, vr, { charset, littleEndian: state.littleEndian, mislabel: state.mislabel });
32027
+ }
32028
+ }
32029
+ return ok(void 0);
32030
+ }
32031
+ function inflateDeflated(byteArray, position) {
32032
+ const inflated = inflateRawSync(byteArray.subarray(position));
32033
+ const combined = new Uint8Array(position + inflated.byteLength);
32034
+ combined.set(byteArray.subarray(0, position), 0);
32035
+ combined.set(inflated, position);
32036
+ return combined;
32037
+ }
32038
+ function implicitVrLookup(tag2) {
32039
+ return lookupTag(tag2.slice(1))?.vr;
32040
+ }
32041
+ function parseDicomBuffer(buffer, options) {
32042
+ let dataSet;
32043
+ try {
32044
+ dataSet = dicomParser2.parseDicom(buffer, { vrCallback: implicitVrLookup, inflater: inflateDeflated });
32045
+ } catch (error) {
32046
+ return err(new Error(`Failed to parse DICOM data: ${stderr(error).message}`));
32047
+ }
32048
+ const littleEndian = dataSet.string("x00020010") !== "1.2.840.10008.1.2.2";
32049
+ const rootCharset = resolveCharsetContext(void 0, options?.charsetAssume, options?.charsetFallback);
32050
+ if (!rootCharset.ok) {
32051
+ return err(rootCharset.error);
32052
+ }
32053
+ const data = {};
32054
+ const stack = [{ src: dataSet, out: data, parentCharset: rootCharset.value }];
32055
+ const mislabel = { promote: options?.utf8MislabelPromote === true, warnings: [], seen: /* @__PURE__ */ new Set() };
32056
+ const state = { littleEndian, options, mislabel };
32057
+ let processed = 0;
32058
+ while (stack.length > 0) {
32059
+ processed++;
32060
+ if (processed > MAX_DATASETS) {
32061
+ return err(new Error(`Failed to parse DICOM data: more than ${String(MAX_DATASETS)} nested datasets`));
32062
+ }
32063
+ const unit = stack.pop();
32064
+ if (unit === void 0) break;
32065
+ const converted = convertDataset(unit, stack, state);
32066
+ if (!converted.ok) {
32067
+ return err(converted.error);
32068
+ }
32069
+ }
32070
+ return ok({ data, warnings: [...dataSet.warnings, ...mislabel.warnings] });
32071
+ }
32072
+ var TS_IMPLICIT_LE = "1.2.840.10008.1.2";
32073
+ var TS_EXPLICIT_BE = "1.2.840.10008.1.2.2";
32074
+ var TS_DEFLATED_LE = "1.2.840.10008.1.2.1.99";
32075
+ var BULK_VRS = /* @__PURE__ */ new Set(["OB", "OW", "OD", "OF", "OL", "OV", "UN"]);
32076
+ var LONG_FORM_VRS = /* @__PURE__ */ new Set(["OB", "OW", "OF", "OD", "OL", "OV", "SQ", "UC", "UR", "UT", "UN", "SV", "UV"]);
32077
+ var UNDEFINED_LENGTH = 4294967295;
32078
+ var MAX_HEADER_BYTES = 12;
32079
+ var MAX_ITERATIONS = 1e4;
32080
+ var MAX_FRAGMENT_HOPS = 1e5;
32081
+ var MAX_READ_CALLS = 1e4;
32082
+ async function readRange(handle, position, length) {
32083
+ const buffer = Buffer.alloc(length);
32084
+ let total = 0;
32085
+ for (let i = 0; i < MAX_READ_CALLS && total < length; i++) {
32086
+ const { bytesRead } = await handle.read(buffer, total, length - total, position + total);
32087
+ if (bytesRead === 0) {
32088
+ return err(new Error(`unexpected EOF at offset ${String(position + total)}`));
32089
+ }
32090
+ total += bytesRead;
32091
+ }
32092
+ if (total < length) {
32093
+ return err(new Error(`short read at offset ${String(position)}`));
32094
+ }
32095
+ return ok(buffer);
32096
+ }
32097
+ function checkAbort(state) {
32098
+ if (state.signal?.aborted === true) {
32099
+ return new Error("aborted");
32100
+ }
32101
+ if (Date.now() > state.deadline) {
32102
+ return new Error(`timed out after ${String(state.timeoutMs)}ms`);
32103
+ }
32104
+ return void 0;
32105
+ }
32106
+ var readPart10Header = dicomParser2.readPart10Header;
32107
+ function probeMeta(head) {
32108
+ let meta;
32109
+ try {
32110
+ meta = readPart10Header(head);
32111
+ } catch {
32112
+ return void 0;
32113
+ }
32114
+ const tsElement = meta.elements["x00020010"];
32115
+ if (meta.position === void 0 || tsElement === void 0) {
32116
+ return void 0;
32117
+ }
32118
+ const transferSyntax = head.subarray(tsElement.dataOffset, tsElement.dataOffset + tsElement.length).toString("latin1").replace(/[\0 ]+$/u, "");
32119
+ return { metaEnd: meta.position, transferSyntax };
32120
+ }
32121
+ function hasPartialDataSet(e) {
32122
+ return typeof e === "object" && e !== null && "dataSet" in e;
32123
+ }
32124
+ function probeParse(assembled) {
32125
+ try {
32126
+ return { dataSet: dicomParser2.parseDicom(assembled, { vrCallback: implicitVrLookup }), threw: false };
32127
+ } catch (e) {
32128
+ return { dataSet: hasPartialDataSet(e) ? e.dataSet : void 0, threw: true };
32129
+ }
32130
+ }
32131
+ function maxExtentElement(dataSet) {
32132
+ let best;
32133
+ let bestEnd = -1;
32134
+ for (const key of Object.keys(dataSet.elements)) {
32135
+ const element = dataSet.elements[key];
32136
+ if (element === void 0) continue;
32137
+ const end = element.dataOffset + element.length;
32138
+ if (end > bestEnd) {
32139
+ bestEnd = end;
32140
+ best = element;
32141
+ }
32142
+ }
32143
+ return best;
32144
+ }
32145
+ function formatTag(group, elementNum) {
32146
+ return group.toString(16).padStart(4, "0") + elementNum.toString(16).padStart(4, "0");
32147
+ }
32148
+ function parseElementHeader(assembled, position, state) {
32149
+ if (position + MAX_HEADER_BYTES > assembled.length) {
32150
+ return void 0;
32151
+ }
32152
+ if (!state.explicitVr) {
32153
+ const tag3 = formatTag(assembled.readUInt16LE(position), assembled.readUInt16LE(position + 2));
32154
+ const vr2 = implicitVrLookup(`x${tag3}`) ?? "UN";
32155
+ return { tag: tag3, vr: vr2, length: assembled.readUInt32LE(position + 4), headerLen: 8, lengthOffset: 4 };
32156
+ }
32157
+ const tag2 = state.bigEndian ? formatTag(assembled.readUInt16BE(position), assembled.readUInt16BE(position + 2)) : formatTag(assembled.readUInt16LE(position), assembled.readUInt16LE(position + 2));
32158
+ const vr = assembled.subarray(position + 4, position + 6).toString("latin1");
32159
+ if (!/^[A-Z]{2}$/u.test(vr)) {
32160
+ return void 0;
32161
+ }
32162
+ if (LONG_FORM_VRS.has(vr)) {
32163
+ const length2 = state.bigEndian ? assembled.readUInt32BE(position + 8) : assembled.readUInt32LE(position + 8);
32164
+ return { tag: tag2, vr, length: length2, headerLen: 12, lengthOffset: 8 };
32165
+ }
32166
+ const length = state.bigEndian ? assembled.readUInt16BE(position + 6) : assembled.readUInt16LE(position + 6);
32167
+ return { tag: tag2, vr, length, headerLen: 8, lengthOffset: 6 };
32168
+ }
32169
+ function decideAfterThrow(probe, state) {
32170
+ if (state.filePos >= state.fileSize) {
32171
+ return { kind: "fullRead" };
32172
+ }
32173
+ if (probe.dataSet === void 0) {
32174
+ return { kind: "grow" };
32175
+ }
32176
+ const last = maxExtentElement(probe.dataSet);
32177
+ const maxEnd = last === void 0 ? state.metaEnd : last.dataOffset + last.length;
32178
+ if (last !== void 0 && maxEnd > state.assembled.length) {
32179
+ return decideOversized(last, last.hadUndefinedLength === true, state);
32180
+ }
32181
+ const header = parseElementHeader(state.assembled, maxEnd, state);
32182
+ if (header === void 0) {
32183
+ return { kind: "grow" };
32184
+ }
32185
+ return decideWithHeader(state, maxEnd, header, header.length === UNDEFINED_LENGTH);
32186
+ }
32187
+ function decideWithHeader(state, position, header, undefinedLength) {
32188
+ if (undefinedLength) {
32189
+ if (header.tag === "7fe00010" && (header.vr === "OB" || header.vr === "OW") && state.explicitVr && !state.bigEndian) {
32190
+ rewriteHeader(state.assembled, position, header, true);
32191
+ return { kind: "skipEncapsulated", headerStart: position, headerEnd: position + header.headerLen };
32192
+ }
32193
+ return { kind: "grow" };
32194
+ }
32195
+ if (BULK_VRS.has(header.vr)) {
32196
+ return rewriteAndSkip(state, position, header);
32197
+ }
32198
+ return { kind: "grow" };
32199
+ }
32200
+ function rewriteHeader(assembled, position, header, forceObVr) {
32201
+ if (header.lengthOffset === 6) {
32202
+ assembled.writeUInt16LE(0, position + header.lengthOffset);
32203
+ } else {
32204
+ assembled.writeUInt32LE(0, position + header.lengthOffset);
32205
+ }
32206
+ if (forceObVr) {
32207
+ assembled.write("OB", position + 4, "latin1");
32208
+ }
32209
+ }
32210
+ function rewriteAndSkip(state, position, header) {
32211
+ rewriteHeader(state.assembled, position, header, false);
32212
+ return { kind: "skipDefined", headerEnd: position + header.headerLen, valueLength: header.length };
32213
+ }
32214
+ function decideAfterSuccess(probe, state) {
32215
+ if (probe.dataSet === void 0) return { kind: "fullRead" };
32216
+ const last = maxExtentElement(probe.dataSet);
32217
+ const maxEnd = last === void 0 ? state.metaEnd : last.dataOffset + last.length;
32218
+ const undefinedAtEnd = last?.hadUndefinedLength === true && maxEnd >= state.assembled.length;
32219
+ if (state.filePos >= state.fileSize) {
32220
+ return { kind: "done" };
32221
+ }
32222
+ if (maxEnd <= state.assembled.length && !undefinedAtEnd) {
32223
+ return { kind: "grow" };
32224
+ }
32225
+ if (last === void 0) return { kind: "grow" };
32226
+ return decideOversized(last, undefinedAtEnd, state);
32227
+ }
32228
+ function decideOversized(last, undefinedAtEnd, state) {
32229
+ const headerStart = findHeaderStart(state.assembled, last, state);
32230
+ if (headerStart === void 0) {
32231
+ return { kind: "grow" };
32232
+ }
32233
+ const header = parseElementHeader(state.assembled, headerStart, state);
32234
+ if (header === void 0) return { kind: "grow" };
32235
+ if (!undefinedAtEnd && header.length !== last.length) {
32236
+ return { kind: "grow" };
32237
+ }
32238
+ return decideWithHeader(state, headerStart, header, undefinedAtEnd);
32239
+ }
32240
+ function findHeaderStart(assembled, element, state) {
32241
+ const candidates = state.explicitVr ? [12, 8] : [8];
32242
+ for (const headerLen of candidates) {
32243
+ const start = element.dataOffset - headerLen;
32244
+ if (start < 0) continue;
32245
+ const header = parseElementHeader(assembled, start, state);
32246
+ if (header !== void 0 && header.headerLen === headerLen) {
32247
+ return start;
32248
+ }
32249
+ }
32250
+ return void 0;
32251
+ }
32252
+ function fileOffsetOf(state, assembledPos) {
32253
+ return state.filePos - (state.assembled.length - assembledPos);
32254
+ }
32255
+ function applyDefinedSkip(state, headerEnd, valueLength) {
32256
+ const valueStartInFile = fileOffsetOf(state, headerEnd);
32257
+ if (valueStartInFile + valueLength > state.fileSize) {
32258
+ return err(new Error("declared value length extends beyond EOF"));
32259
+ }
32260
+ state.filePos = valueStartInFile + valueLength;
32261
+ state.assembled = state.assembled.subarray(0, headerEnd);
32262
+ state.growSize = state.chunkBytes;
32263
+ return ok(void 0);
32264
+ }
32265
+ async function hopFragments(handle, start, fileSize) {
32266
+ let position = start;
32267
+ for (let i = 0; i < MAX_FRAGMENT_HOPS; i++) {
32268
+ if (position + 8 > fileSize) {
32269
+ return err(new Error("encapsulated data truncated"));
32270
+ }
32271
+ const headerResult = await readRange(handle, position, 8);
32272
+ if (!headerResult.ok) {
32273
+ return err(headerResult.error);
32274
+ }
32275
+ const group = headerResult.value.readUInt16LE(0);
32276
+ const elementNum = headerResult.value.readUInt16LE(2);
32277
+ const itemLength = headerResult.value.readUInt32LE(4);
32278
+ if (group !== 65534) {
32279
+ return err(new Error("unexpected tag between fragments"));
32280
+ }
32281
+ if (elementNum === 57565) {
32282
+ return ok(position + 8);
32283
+ }
32284
+ if (elementNum !== 57344 || itemLength === UNDEFINED_LENGTH) {
32285
+ return err(new Error("malformed fragment item"));
32286
+ }
32287
+ position += 8 + itemLength;
32288
+ }
32289
+ return err(new Error("fragment count exceeded bound"));
32290
+ }
32291
+ async function applyEncapsulatedSkip(state, headerEnd) {
32292
+ const valueStartInFile = fileOffsetOf(state, headerEnd);
32293
+ const afterDelimiter = await hopFragments(state.handle, valueStartInFile, state.fileSize);
32294
+ if (!afterDelimiter.ok) {
32295
+ return err(afterDelimiter.error);
32296
+ }
32297
+ state.filePos = afterDelimiter.value;
32298
+ state.assembled = state.assembled.subarray(0, headerEnd);
32299
+ state.growSize = state.chunkBytes;
32300
+ return ok(void 0);
32301
+ }
32302
+ async function appendChunk(state) {
32303
+ const length = Math.min(state.growSize, state.fileSize - state.filePos);
32304
+ if (length <= 0) return err(new Error("no bytes left to grow"));
32305
+ const chunk = await readRange(state.handle, state.filePos, length);
32306
+ if (!chunk.ok) {
32307
+ return err(chunk.error);
32308
+ }
32309
+ state.assembled = Buffer.concat([state.assembled, chunk.value]);
32310
+ state.filePos += length;
32311
+ state.growSize = Math.min(state.growSize * 2, state.fileSize);
32312
+ return ok(void 0);
32313
+ }
32314
+ async function applyAction(action, state) {
32315
+ switch (action.kind) {
32316
+ case "done":
32317
+ return "done";
32318
+ case "fullRead":
32319
+ return "fullRead";
32320
+ case "grow":
32321
+ return (await appendChunk(state)).ok ? "continue" : "fullRead";
32322
+ case "skipDefined":
32323
+ return applyDefinedSkip(state, action.headerEnd, action.valueLength).ok ? "continue" : "fullRead";
32324
+ case "skipEncapsulated":
32325
+ return (await applyEncapsulatedSkip(state, action.headerEnd)).ok ? "continue" : "fullRead";
32326
+ }
32327
+ }
32328
+ async function boundedLoop(state) {
32329
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
32330
+ const abort = checkAbort(state);
32331
+ if (abort !== void 0) {
32332
+ return err(abort);
32333
+ }
32334
+ const probe = probeParse(state.assembled);
32335
+ const action = probe.threw ? decideAfterThrow(probe, state) : decideAfterSuccess(probe, state);
32336
+ const outcome = await applyAction(action, state);
32337
+ if (outcome === "done") {
32338
+ return ok(state.assembled);
32339
+ }
32340
+ if (outcome === "fullRead") {
32341
+ return "fullRead";
32342
+ }
32343
+ }
32344
+ return "fullRead";
32345
+ }
32346
+ async function readWhole(handle, fileSize, state) {
32347
+ const abort = checkAbort(state);
32348
+ if (abort !== void 0) {
32349
+ return err(abort);
32350
+ }
32351
+ return readRange(handle, 0, fileSize);
32352
+ }
32353
+ async function readWithHandle(handle, options) {
32354
+ const fileSize = (await handle.stat()).size;
32355
+ const deadline = Date.now() + options.timeoutMs;
32356
+ const guard = { deadline, signal: options.signal, timeoutMs: options.timeoutMs };
32357
+ const threshold = options.thresholdBytes ?? BOUNDED_READ_THRESHOLD_BYTES;
32358
+ const chunkBytes = options.chunkBytes ?? BOUNDED_READ_CHUNK_BYTES;
32359
+ if (fileSize <= threshold) {
32360
+ return readWhole(handle, fileSize, guard);
32361
+ }
32362
+ const headResult = await readRange(handle, 0, Math.min(chunkBytes, fileSize));
32363
+ if (!headResult.ok) {
32364
+ return err(headResult.error);
32365
+ }
32366
+ const meta = probeMeta(headResult.value);
32367
+ if (meta === void 0 || meta.transferSyntax === TS_DEFLATED_LE) {
32368
+ return readWhole(handle, fileSize, guard);
32369
+ }
32370
+ const state = {
32371
+ handle,
32372
+ fileSize,
32373
+ chunkBytes,
32374
+ explicitVr: meta.transferSyntax !== TS_IMPLICIT_LE,
32375
+ bigEndian: meta.transferSyntax === TS_EXPLICIT_BE,
32376
+ metaEnd: meta.metaEnd,
32377
+ deadline,
32378
+ timeoutMs: options.timeoutMs,
32379
+ signal: options.signal,
32380
+ assembled: headResult.value,
32381
+ filePos: headResult.value.length,
32382
+ growSize: chunkBytes
32383
+ };
32384
+ const result = await boundedLoop(state);
32385
+ if (result === "fullRead") {
32386
+ return readWhole(handle, fileSize, guard);
32387
+ }
32388
+ return result;
32389
+ }
32390
+ async function readDicomHead(inputPath, options) {
32391
+ let handle;
32392
+ try {
32393
+ handle = await open(inputPath, "r");
32394
+ } catch (error) {
32395
+ const message = error instanceof Error ? error.message : String(error);
32396
+ return err(new Error(`failed to read '${inputPath}': ${message}`));
32397
+ }
32398
+ try {
32399
+ const result = await readWithHandle(handle, options);
32400
+ if (result.ok) {
32401
+ return result;
32402
+ }
32403
+ return err(new Error(`reading '${inputPath}' failed: ${result.error.message}`));
32404
+ } catch (error) {
32405
+ const message = error instanceof Error ? error.message : String(error);
32406
+ return err(new Error(`failed to read '${inputPath}': ${message}`));
32407
+ } finally {
32408
+ await handle.close();
32409
+ }
32410
+ }
32411
+
32412
+ // src/tools/dicom2json.ts
32413
+ var Dicom2jsonOptionsSchema = z.object({
32414
+ timeoutMs: z.number().int().positive().optional(),
32415
+ signal: z.instanceof(AbortSignal).optional(),
32416
+ charsetAssume: z.string().min(1).optional(),
32417
+ charsetFallback: z.string().min(1).optional(),
32418
+ utf8MislabelPromote: z.boolean().optional(),
32419
+ dcmtkFallback: z.boolean().optional(),
32420
+ boundedRead: z.boolean().optional()
32421
+ }).strict().optional();
32422
+ var Dicom2jsonBufferOptionsSchema = z.object({
32423
+ charsetAssume: z.string().min(1).optional(),
32424
+ charsetFallback: z.string().min(1).optional(),
32425
+ utf8MislabelPromote: z.boolean().optional()
32426
+ }).strict().optional();
32427
+ async function readFileBounded(inputPath, timeoutMs, signal) {
32428
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
32429
+ const combined = signal !== void 0 ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
32430
+ try {
32431
+ return ok(await readFile(inputPath, { signal: combined }));
32432
+ } catch (error) {
32433
+ if (timeoutSignal.aborted) {
32434
+ return err(new Error(`dicom2json: reading '${inputPath}' timed out after ${String(timeoutMs)}ms`));
32435
+ }
32436
+ if (signal?.aborted === true) {
32437
+ return err(new Error(`dicom2json: aborted while reading '${inputPath}'`));
32438
+ }
32439
+ const message = error instanceof Error ? error.message : String(error);
32440
+ return err(new Error(`dicom2json: failed to read '${inputPath}': ${message}`));
32441
+ }
32442
+ }
32443
+ async function runDcmtkFallback(inputPath, jsError, remainingMs, options) {
32444
+ if (remainingMs <= 0) {
32445
+ return err(new Error(`dicom2json: JS parse failed and timeout budget exhausted (skipping DCMTK fallback) | js: ${jsError.message}`));
32446
+ }
32447
+ const fallback = await dcm2json(inputPath, {
32448
+ timeoutMs: remainingMs,
32449
+ signal: options?.signal,
32450
+ charsetAssume: options?.charsetAssume,
32451
+ charsetFallback: options?.charsetFallback
32452
+ });
32453
+ if (!fallback.ok) {
32454
+ return err(new Error(`dicom2json: both engines failed | js: ${jsError.message} | dcmtk: ${fallback.error.message}`));
32455
+ }
32456
+ return ok({ data: fallback.value.data, warnings: [], source: fallback.value.source });
32457
+ }
32458
+ async function dicom2json(inputPath, options) {
32459
+ const validation = Dicom2jsonOptionsSchema.safeParse(options);
32460
+ if (!validation.success) {
32461
+ return err(createValidationError("dicom2json", validation.error));
32462
+ }
32463
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
32464
+ const startTime = Date.now();
32465
+ const parsed = await parseFromFile(inputPath, timeoutMs, options);
32466
+ if (parsed.ok) {
32467
+ return parsed;
32468
+ }
32469
+ if (options?.dcmtkFallback === true) {
32470
+ return runDcmtkFallback(inputPath, parsed.error, timeoutMs - (Date.now() - startTime), options);
32471
+ }
32472
+ return err(parsed.error);
32473
+ }
32474
+ async function readHeadBounded(inputPath, timeoutMs, signal) {
32475
+ const result = await readDicomHead(inputPath, { timeoutMs, signal });
32476
+ if (result.ok) {
32477
+ return result;
32478
+ }
32479
+ return err(new Error(`dicom2json: ${result.error.message}`));
32480
+ }
32481
+ async function parseFromFile(inputPath, timeoutMs, options) {
32482
+ const fileResult = options?.boundedRead === false ? await readFileBounded(inputPath, timeoutMs, options?.signal) : await readHeadBounded(inputPath, timeoutMs, options?.signal);
32483
+ if (!fileResult.ok) {
32484
+ return err(fileResult.error);
32485
+ }
32486
+ const parsed = parseDicomBuffer(fileResult.value, {
32487
+ charsetAssume: options?.charsetAssume,
32488
+ charsetFallback: options?.charsetFallback,
32489
+ utf8MislabelPromote: options?.utf8MislabelPromote
32490
+ });
32491
+ if (!parsed.ok) {
32492
+ return err(parsed.error);
32493
+ }
32494
+ return ok({ data: parsed.value.data, warnings: parsed.value.warnings, source: "js" });
32495
+ }
32496
+ function dicom2jsonFromBuffer(buffer, options) {
32497
+ const validation = Dicom2jsonBufferOptionsSchema.safeParse(options);
32498
+ if (!validation.success) {
32499
+ return err(createValidationError("dicom2jsonFromBuffer", validation.error));
32500
+ }
32501
+ const parsed = parseDicomBuffer(buffer, options);
32502
+ if (!parsed.ok) {
32503
+ return err(parsed.error);
32504
+ }
32505
+ return ok({ data: parsed.value.data, warnings: parsed.value.warnings, source: "js" });
31559
32506
  }
31560
32507
  var DcmdumpFormat = {
31561
32508
  /** Print standard DCMTK format. */
@@ -34368,16 +35315,56 @@ async function unlinkFile(path2) {
34368
35315
  }
34369
35316
 
34370
35317
  // src/dicom/DicomInstance.ts
35318
+ async function readViaDcmtk(path2, shared) {
35319
+ const result = await dcm2json(path2, shared);
35320
+ if (!result.ok) return err(result.error);
35321
+ return ok({ data: result.value.data, warnings: [] });
35322
+ }
35323
+ async function readViaJs(path2, shared, options) {
35324
+ const result = await dicom2json(path2, {
35325
+ ...shared,
35326
+ utf8MislabelPromote: options?.utf8MislabelPromote,
35327
+ boundedRead: options?.boundedRead,
35328
+ dcmtkFallback: true
35329
+ });
35330
+ if (!result.ok) return err(result.error);
35331
+ return ok({ data: result.value.data, warnings: result.value.warnings });
35332
+ }
35333
+ async function readDicomJson(path2, options) {
35334
+ const shared = {
35335
+ timeoutMs: options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
35336
+ signal: options?.signal,
35337
+ charsetAssume: options?.charsetAssume,
35338
+ charsetFallback: options?.charsetFallback
35339
+ };
35340
+ if (options?.engine === "dcmtk") {
35341
+ return readViaDcmtk(path2, shared);
35342
+ }
35343
+ return readViaJs(path2, shared, options);
35344
+ }
34371
35345
  var DicomInstance = class _DicomInstance {
34372
- constructor(dataset, changes, filePath, metadata) {
35346
+ constructor(state) {
34373
35347
  __publicField(this, "dicomDataset");
34374
35348
  __publicField(this, "changeSet");
34375
35349
  __publicField(this, "filepath");
34376
35350
  __publicField(this, "meta");
34377
- this.dicomDataset = dataset;
34378
- this.changeSet = changes;
34379
- this.filepath = filePath;
34380
- this.meta = metadata;
35351
+ __publicField(this, "warningList");
35352
+ this.dicomDataset = state.dataset;
35353
+ this.changeSet = state.changes;
35354
+ this.filepath = state.filePath;
35355
+ this.meta = state.metadata;
35356
+ this.warningList = state.warnings;
35357
+ }
35358
+ /** Creates a copy with the given fields replaced. */
35359
+ derive(overrides) {
35360
+ return new _DicomInstance({
35361
+ dataset: this.dicomDataset,
35362
+ changes: this.changeSet,
35363
+ filePath: this.filepath,
35364
+ metadata: this.meta,
35365
+ warnings: this.warningList,
35366
+ ...overrides
35367
+ });
34381
35368
  }
34382
35369
  // -----------------------------------------------------------------------
34383
35370
  // Factories
@@ -34392,16 +35379,19 @@ var DicomInstance = class _DicomInstance {
34392
35379
  static async open(path2, options) {
34393
35380
  const filePathResult = createDicomFilePath(path2);
34394
35381
  if (!filePathResult.ok) return err(filePathResult.error);
34395
- const jsonResult = await dcm2json(path2, {
34396
- timeoutMs: options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
34397
- signal: options?.signal,
34398
- charsetAssume: options?.charsetAssume,
34399
- charsetFallback: options?.charsetFallback
34400
- });
35382
+ const jsonResult = await readDicomJson(path2, options);
34401
35383
  if (!jsonResult.ok) return err(jsonResult.error);
34402
35384
  const datasetResult = DicomDataset.fromJson(jsonResult.value.data);
34403
35385
  if (!datasetResult.ok) return err(datasetResult.error);
34404
- return ok(new _DicomInstance(datasetResult.value, ChangeSet.empty(), filePathResult.value, /* @__PURE__ */ new Map()));
35386
+ return ok(
35387
+ new _DicomInstance({
35388
+ dataset: datasetResult.value,
35389
+ changes: ChangeSet.empty(),
35390
+ filePath: filePathResult.value,
35391
+ metadata: /* @__PURE__ */ new Map(),
35392
+ warnings: jsonResult.value.warnings
35393
+ })
35394
+ );
34405
35395
  }
34406
35396
  /**
34407
35397
  * Creates a DicomInstance from an existing DicomDataset.
@@ -34417,7 +35407,7 @@ var DicomInstance = class _DicomInstance {
34417
35407
  if (!fpResult.ok) return err(fpResult.error);
34418
35408
  fp = fpResult.value;
34419
35409
  }
34420
- return ok(new _DicomInstance(dataset, ChangeSet.empty(), fp, /* @__PURE__ */ new Map()));
35410
+ return ok(new _DicomInstance({ dataset, changes: ChangeSet.empty(), filePath: fp, metadata: /* @__PURE__ */ new Map(), warnings: [] }));
34421
35411
  }
34422
35412
  // -----------------------------------------------------------------------
34423
35413
  // Read accessors (delegate to DicomDataset)
@@ -34438,6 +35428,15 @@ var DicomInstance = class _DicomInstance {
34438
35428
  get filePath() {
34439
35429
  return this.filepath;
34440
35430
  }
35431
+ /**
35432
+ * Non-fatal parser warnings from opening the file (e.g. `possible UTF-8 mislabel: <tag>`).
35433
+ *
35434
+ * Empty for instances created via {@link DicomInstance.fromDataset} or opened with
35435
+ * `engine: 'dcmtk'`. Preserved across fluent modifications.
35436
+ */
35437
+ get warnings() {
35438
+ return this.warningList;
35439
+ }
34441
35440
  /** Patient's Name (0010,0010). */
34442
35441
  get patientName() {
34443
35442
  return this.dicomDataset.patientName;
@@ -34517,7 +35516,7 @@ var DicomInstance = class _DicomInstance {
34517
35516
  * @param value - The new value
34518
35517
  */
34519
35518
  setTag(path2, value) {
34520
- return new _DicomInstance(this.dicomDataset, this.changeSet.setTag(path2, value), this.filepath, this.meta);
35519
+ return this.derive({ changes: this.changeSet.setTag(path2, value) });
34521
35520
  }
34522
35521
  /**
34523
35522
  * Erases a tag, returning a new DicomInstance.
@@ -34525,11 +35524,11 @@ var DicomInstance = class _DicomInstance {
34525
35524
  * @param path - The DICOM tag path to erase
34526
35525
  */
34527
35526
  eraseTag(path2) {
34528
- return new _DicomInstance(this.dicomDataset, this.changeSet.eraseTag(path2), this.filepath, this.meta);
35527
+ return this.derive({ changes: this.changeSet.eraseTag(path2) });
34529
35528
  }
34530
35529
  /** Erases all private tags, returning a new DicomInstance. */
34531
35530
  erasePrivateTags() {
34532
- return new _DicomInstance(this.dicomDataset, this.changeSet.erasePrivateTags(), this.filepath, this.meta);
35531
+ return this.derive({ changes: this.changeSet.erasePrivateTags() });
34533
35532
  }
34534
35533
  /** Sets Patient's Name (0010,0010). */
34535
35534
  setPatientName(value) {
@@ -34583,7 +35582,7 @@ var DicomInstance = class _DicomInstance {
34583
35582
  * @param entries - A record of tag path → value pairs
34584
35583
  */
34585
35584
  setBatch(entries) {
34586
- return new _DicomInstance(this.dicomDataset, this.changeSet.setBatch(entries), this.filepath, this.meta);
35585
+ return this.derive({ changes: this.changeSet.setBatch(entries) });
34587
35586
  }
34588
35587
  /**
34589
35588
  * Returns a new DicomInstance with the given changes merged into pending changes.
@@ -34592,7 +35591,7 @@ var DicomInstance = class _DicomInstance {
34592
35591
  * @returns A new DicomInstance with accumulated changes
34593
35592
  */
34594
35593
  withChanges(changes) {
34595
- return new _DicomInstance(this.dicomDataset, this.changeSet.merge(changes), this.filepath, this.meta);
35594
+ return this.derive({ changes: this.changeSet.merge(changes) });
34596
35595
  }
34597
35596
  /**
34598
35597
  * Returns a new DicomInstance pointing to a different file path.
@@ -34606,7 +35605,7 @@ var DicomInstance = class _DicomInstance {
34606
35605
  withFilePath(newPath) {
34607
35606
  const result = createDicomFilePath(newPath);
34608
35607
  if (!result.ok) throw result.error;
34609
- return new _DicomInstance(this.dicomDataset, this.changeSet, result.value, this.meta);
35608
+ return this.derive({ filePath: result.value });
34610
35609
  }
34611
35610
  // -----------------------------------------------------------------------
34612
35611
  // File I/O
@@ -34644,7 +35643,7 @@ var DicomInstance = class _DicomInstance {
34644
35643
  return err(applyResult.error);
34645
35644
  }
34646
35645
  }
34647
- return ok(new _DicomInstance(this.dicomDataset, ChangeSet.empty(), outPathResult.value, this.meta));
35646
+ return ok(this.derive({ changes: ChangeSet.empty(), filePath: outPathResult.value }));
34648
35647
  }
34649
35648
  /**
34650
35649
  * Gets the file size in bytes.
@@ -34679,7 +35678,7 @@ var DicomInstance = class _DicomInstance {
34679
35678
  withMetadata(key, value) {
34680
35679
  const newMeta = new Map(this.meta);
34681
35680
  newMeta.set(key, value);
34682
- return new _DicomInstance(this.dicomDataset, this.changeSet, this.filepath, newMeta);
35681
+ return this.derive({ metadata: newMeta });
34683
35682
  }
34684
35683
  /**
34685
35684
  * Gets application metadata by key.
@@ -36486,7 +37485,10 @@ var DicomReceiverOptionsSchema = z.object({
36486
37485
  timeoutMs: z.number().int().positive().optional(),
36487
37486
  signal: z.instanceof(AbortSignal).optional(),
36488
37487
  charsetAssume: z.string().min(1).optional(),
36489
- charsetFallback: z.string().min(1).optional()
37488
+ charsetFallback: z.string().min(1).optional(),
37489
+ utf8MislabelPromote: z.boolean().optional(),
37490
+ boundedRead: z.boolean().optional(),
37491
+ engine: z.enum(["js", "dcmtk"]).optional()
36490
37492
  }).strict().optional(),
36491
37493
  parseInstances: z.boolean().optional(),
36492
37494
  signal: z.instanceof(AbortSignal).optional()
@@ -37927,8 +38929,8 @@ function createStorescuExecutor(options) {
37927
38929
  if (!result.ok) {
37928
38930
  const e = result.error;
37929
38931
  const stdout = e instanceof ToolExecutionError ? e.stdout : "";
37930
- const stderr8 = e instanceof ToolExecutionError ? e.stderr : "";
37931
- return { stdout, stderr: stderr8, error: e };
38932
+ const stderr9 = e instanceof ToolExecutionError ? e.stderr : "";
38933
+ return { stdout, stderr: stderr9, error: e };
37932
38934
  }
37933
38935
  return { stdout: result.value.stdout, stderr: result.value.stderr };
37934
38936
  };
@@ -38185,8 +39187,8 @@ function createDcmsendExecutor(options) {
38185
39187
  if (!result.ok) {
38186
39188
  const e = result.error;
38187
39189
  const stdout = e instanceof ToolExecutionError ? e.stdout : "";
38188
- const stderr8 = e instanceof ToolExecutionError ? e.stderr : "";
38189
- return { stdout, stderr: stderr8, error: e };
39190
+ const stderr9 = e instanceof ToolExecutionError ? e.stderr : "";
39191
+ return { stdout, stderr: stderr9, error: e };
38190
39192
  }
38191
39193
  return { stdout: result.value.stdout, stderr: result.value.stderr };
38192
39194
  };
@@ -38526,7 +39528,7 @@ async function cleanupTempDir(directory) {
38526
39528
  }
38527
39529
  }
38528
39530
  async function parseSingleFile(filePath, timeoutMs, signal) {
38529
- const jsonResult = await dcm2json(filePath, { timeoutMs, signal });
39531
+ const jsonResult = await dicom2json(filePath, { timeoutMs, signal, dcmtkFallback: true });
38530
39532
  if (!jsonResult.ok) {
38531
39533
  return err(jsonResult.error);
38532
39534
  }
@@ -39176,6 +40178,6 @@ var DicomHammer = class _DicomHammer extends EventEmitter {
39176
40178
  }
39177
40179
  };
39178
40180
 
39179
- export { AETitleSchema, AssociationTracker, ChangeSet, ColorConversion, DCMPRSCP_FATAL_EVENTS, DCMPRSCP_PATTERNS, DCMPSRCV_FATAL_EVENTS, DCMPSRCV_PATTERNS, DCMQRSCP_FATAL_EVENTS, DCMQRSCP_PATTERNS, DCMRECV_FATAL_EVENTS, DCMRECV_PATTERNS, DEFAULT_BLOCK_TIMEOUT_MS, DEFAULT_DICOM_PORT, DEFAULT_DRAIN_TIMEOUT_MS, DEFAULT_PARSE_CONCURRENCY, DEFAULT_START_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, Dcm2pnmOutputFormat, Dcm2xmlCharset, DcmQRSCP, DcmdumpFormat, Dcmj2pnmOutputFormat, DcmprsCP, DcmprscpEvent, Dcmpsrcv, DcmpsrcvEvent, DcmqrscpEvent, Dcmrecv, DcmrecvEvent, DcmtkProcess, DicomDataset, DicomHammer, DicomInstance, DicomReceiver, DicomSend, DicomSender, DicomTagPathSchema, DicomTagSchema, FilenameMode, GetQueryModel, HammerPhase, Img2dcmInputFormat, JplsColorConversion, LineParser, LutType, MAX_BLOCK_LINES, MAX_CHANGESET_OPERATIONS, MAX_EVENT_PATTERNS, MAX_TRAVERSAL_DEPTH, MoveQueryModel, PDU_SIZE, PacsClient, PortSchema, PreferredTransferSyntax, ProcessState, ProposedTransferSyntax, QueryLevel, QueryModel, REQUIRED_BINARIES, RetrieveMode, SOP_CLASSES, STORESCP_FATAL_EVENTS, STORESCP_PATTERNS, SenderHealth, SenderMode, StorageMode, StoreSCP, StoreSCPPreset, StorescpEvent, SubdirectoryMode, ToolExecutionError, TransferSyntax, UIDSchema, UNIX_SEARCH_PATHS, VR, VR_CATEGORY, VR_CATEGORY_NAME, VR_META, WINDOWS_SEARCH_PATHS, WLMSCPFS_FATAL_EVENTS, WLMSCPFS_PATTERNS, Wlmscpfs, WlmscpfsEvent, assertUnreachable, batch, cda2dcm, clearDcmtkPathCache, createAETitle, createDicomFilePath, createDicomTag, createDicomTagPath, createPort, createSOPClassUID, createTransferSyntaxUID, dcm2cda, dcm2json, dcm2pdf, dcm2pnm, dcm2xml, dcmcjpeg, dcmcjpls, dcmconv, dcmcrle, dcmdecap, dcmdjpeg, dcmdjpls, dcmdrle, dcmdspfn, dcmdump, dcmencap, dcmftest, dcmgpdir, dcmj2pnm, dcmmkcrv, dcmmkdir, dcmmklut, dcmodify, dcmp2pgm, dcmprscu, dcmpschk, dcmpsmk, dcmpsprt, dcmqridx, dcmquant, dcmscale, dcmsend, dcod2lum, dconvlum, drtdump, dsr2xml, dsrdump, dump2dcm, echoscu, err, execCommand, findDcmtkPath, findscu, getVRCategory, getscu, img2dcm, isBinaryVR, isNumericVR, isStringVR, json2dcm, lookupTag, lookupTagByKeyword, lookupTagByName, mapResult, movescu, ok, parseAETitle, parseDicomTag, parseDicomTagPath, parsePort, parseSOPClassUID, parseTransferSyntaxUID, pdf2dcm, retry, segmentsToModifyPath, segmentsToString, sopClassNameFromUID, spawnCommand, stl2dcm, storescu, tag, tagPathToSegments, termscu, walkTags, xml2dcm, xml2dsr, xmlToJson };
40181
+ export { AETitleSchema, AssociationTracker, ChangeSet, ColorConversion, DCMPRSCP_FATAL_EVENTS, DCMPRSCP_PATTERNS, DCMPSRCV_FATAL_EVENTS, DCMPSRCV_PATTERNS, DCMQRSCP_FATAL_EVENTS, DCMQRSCP_PATTERNS, DCMRECV_FATAL_EVENTS, DCMRECV_PATTERNS, DEFAULT_BLOCK_TIMEOUT_MS, DEFAULT_DICOM_PORT, DEFAULT_DRAIN_TIMEOUT_MS, DEFAULT_PARSE_CONCURRENCY, DEFAULT_START_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, Dcm2pnmOutputFormat, Dcm2xmlCharset, DcmQRSCP, DcmdumpFormat, Dcmj2pnmOutputFormat, DcmprsCP, DcmprscpEvent, Dcmpsrcv, DcmpsrcvEvent, DcmqrscpEvent, Dcmrecv, DcmrecvEvent, DcmtkProcess, DicomDataset, DicomHammer, DicomInstance, DicomReceiver, DicomSend, DicomSender, DicomTagPathSchema, DicomTagSchema, FilenameMode, GetQueryModel, HammerPhase, Img2dcmInputFormat, JplsColorConversion, LineParser, LutType, MAX_BLOCK_LINES, MAX_CHANGESET_OPERATIONS, MAX_EVENT_PATTERNS, MAX_TRAVERSAL_DEPTH, MoveQueryModel, PDU_SIZE, PacsClient, PortSchema, PreferredTransferSyntax, ProcessState, ProposedTransferSyntax, QueryLevel, QueryModel, REQUIRED_BINARIES, RetrieveMode, SOP_CLASSES, STORESCP_FATAL_EVENTS, STORESCP_PATTERNS, SenderHealth, SenderMode, StorageMode, StoreSCP, StoreSCPPreset, StorescpEvent, SubdirectoryMode, ToolExecutionError, TransferSyntax, UIDSchema, UNIX_SEARCH_PATHS, VR, VR_CATEGORY, VR_CATEGORY_NAME, VR_META, WINDOWS_SEARCH_PATHS, WLMSCPFS_FATAL_EVENTS, WLMSCPFS_PATTERNS, Wlmscpfs, WlmscpfsEvent, assertUnreachable, batch, cda2dcm, clearDcmtkPathCache, createAETitle, createDicomFilePath, createDicomTag, createDicomTagPath, createPort, createSOPClassUID, createTransferSyntaxUID, dcm2cda, dcm2json, dcm2pdf, dcm2pnm, dcm2xml, dcmcjpeg, dcmcjpls, dcmconv, dcmcrle, dcmdecap, dcmdjpeg, dcmdjpls, dcmdrle, dcmdspfn, dcmdump, dcmencap, dcmftest, dcmgpdir, dcmj2pnm, dcmmkcrv, dcmmkdir, dcmmklut, dcmodify, dcmp2pgm, dcmprscu, dcmpschk, dcmpsmk, dcmpsprt, dcmqridx, dcmquant, dcmscale, dcmsend, dcod2lum, dconvlum, dicom2json, dicom2jsonFromBuffer, drtdump, dsr2xml, dsrdump, dump2dcm, echoscu, err, execCommand, findDcmtkPath, findscu, getVRCategory, getscu, img2dcm, isBinaryVR, isNumericVR, isStringVR, json2dcm, lookupTag, lookupTagByKeyword, lookupTagByName, mapResult, movescu, ok, parseAETitle, parseDicomTag, parseDicomTagPath, parsePort, parseSOPClassUID, parseTransferSyntaxUID, pdf2dcm, retry, segmentsToModifyPath, segmentsToString, sopClassNameFromUID, spawnCommand, stl2dcm, storescu, tag, tagPathToSegments, termscu, walkTags, xml2dcm, xml2dsr, xmlToJson };
39180
40182
  //# sourceMappingURL=index.js.map
39181
40183
  //# sourceMappingURL=index.js.map