@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.cjs CHANGED
@@ -10,6 +10,8 @@ var stderrLib = require('stderr-lib');
10
10
  var promises$1 = require('fs/promises');
11
11
  var os = require('os');
12
12
  var fastXmlParser = require('fast-xml-parser');
13
+ var zlib = require('zlib');
14
+ var dicomParser2 = require('dicom-parser');
13
15
  var net = require('net');
14
16
  var promises = require('timers/promises');
15
17
  var crypto = require('crypto');
@@ -37,6 +39,7 @@ function _interopNamespace(e) {
37
39
  var path__namespace = /*#__PURE__*/_interopNamespace(path);
38
40
  var kill__default = /*#__PURE__*/_interopDefault(kill);
39
41
  var os__namespace = /*#__PURE__*/_interopNamespace(os);
42
+ var dicomParser2__default = /*#__PURE__*/_interopDefault(dicomParser2);
40
43
  var net__namespace = /*#__PURE__*/_interopNamespace(net);
41
44
 
42
45
  var __defProp = Object.defineProperty;
@@ -217,6 +220,8 @@ var MAX_EVENT_PATTERNS = 200;
217
220
  var MAX_TRAVERSAL_DEPTH = 50;
218
221
  var MAX_CHANGESET_OPERATIONS = 1e4;
219
222
  var MAX_OUTPUT_BYTES = 100 * 1024 * 1024;
223
+ var BOUNDED_READ_THRESHOLD_BYTES = 8 * 1024 * 1024;
224
+ var BOUNDED_READ_CHUNK_BYTES = 1024 * 1024;
220
225
  var cachedPath;
221
226
  var isWindows = process.platform === "win32";
222
227
  function binaryName(name) {
@@ -323,7 +328,7 @@ async function execCommand(binary, args, options) {
323
328
  }
324
329
  function wireSpawnListeners(binary, child, timeoutMs, resolve) {
325
330
  let stdout = "";
326
- let stderr8 = "";
331
+ let stderr9 = "";
327
332
  let settled = false;
328
333
  const name = binary.split("/").pop() ?? binary;
329
334
  const settle = (result) => {
@@ -338,14 +343,14 @@ function wireSpawnListeners(binary, child, timeoutMs, resolve) {
338
343
  }, timeoutMs);
339
344
  child.stdout?.on("data", (chunk) => {
340
345
  stdout += String(chunk);
341
- if (stdout.length + stderr8.length > MAX_OUTPUT_BYTES) {
346
+ if (stdout.length + stderr9.length > MAX_OUTPUT_BYTES) {
342
347
  if (child.pid !== void 0 && child.pid !== null) killTree(child.pid);
343
348
  settle(err(new Error(`${name} output exceeded ${MAX_OUTPUT_BYTES} bytes`)));
344
349
  }
345
350
  });
346
351
  child.stderr?.on("data", (chunk) => {
347
- stderr8 += String(chunk);
348
- if (stdout.length + stderr8.length > MAX_OUTPUT_BYTES) {
352
+ stderr9 += String(chunk);
353
+ if (stdout.length + stderr9.length > MAX_OUTPUT_BYTES) {
349
354
  if (child.pid !== void 0 && child.pid !== null) killTree(child.pid);
350
355
  settle(err(new Error(`${name} output exceeded ${MAX_OUTPUT_BYTES} bytes`)));
351
356
  }
@@ -355,7 +360,7 @@ function wireSpawnListeners(binary, child, timeoutMs, resolve) {
355
360
  settle(err(new Error(`${name} error: ${error.message}`)));
356
361
  });
357
362
  child.on("close", (code) => {
358
- settle(ok({ stdout, stderr: stderr8, exitCode: code ?? 1 }));
363
+ settle(ok({ stdout, stderr: stderr9, exitCode: code ?? 1 }));
359
364
  });
360
365
  }
361
366
  async function spawnCommand(binary, args, options) {
@@ -734,9 +739,9 @@ var ToolExecutionError = class extends Error {
734
739
  this.exitCode = details.exitCode;
735
740
  }
736
741
  };
737
- function createToolError(toolName, args, exitCode, stderr8, stdout = "") {
742
+ function createToolError(toolName, args, exitCode, stderr9, stdout = "") {
738
743
  const argsStr = truncate(args.join(" "), MAX_ARGS_LENGTH);
739
- const stderrStr = truncate(stderr8.trim(), MAX_STDERR_LENGTH);
744
+ const stderrStr = truncate(stderr9.trim(), MAX_STDERR_LENGTH);
740
745
  const parts = [`${toolName} failed (exit code ${String(exitCode)})`];
741
746
  if (argsStr.length > 0) {
742
747
  parts.push(`args: ${argsStr}`);
@@ -744,7 +749,7 @@ function createToolError(toolName, args, exitCode, stderr8, stdout = "") {
744
749
  if (stderrStr.length > 0) {
745
750
  parts.push(`stderr: ${stderrStr}`);
746
751
  }
747
- return new ToolExecutionError(parts.join(" | "), { stdout, stderr: stderr8, exitCode });
752
+ return new ToolExecutionError(parts.join(" | "), { stdout, stderr: stderr9, exitCode });
748
753
  }
749
754
  function createValidationError(toolName, zodError) {
750
755
  const parts = [];
@@ -31364,8 +31369,8 @@ function unwrapValue(v) {
31364
31369
  const obj = v;
31365
31370
  if ("#text" in obj) return obj["#text"];
31366
31371
  const keys = Object.keys(obj);
31367
- if (keys.length === 1 && keys[0] !== void 0 && keys[0].startsWith("@_")) {
31368
- return obj[keys[0]];
31372
+ if (keys.every((k) => k.startsWith("@_"))) {
31373
+ return "";
31369
31374
  }
31370
31375
  return v;
31371
31376
  }
@@ -31571,16 +31576,959 @@ async function dcm2json(inputPath, options) {
31571
31576
  const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
31572
31577
  const signal = options?.signal;
31573
31578
  const startTime = Date.now();
31574
- const remaining = () => Math.max(1e3, timeoutMs - (Date.now() - startTime));
31579
+ const remaining = () => Math.max(0, timeoutMs - (Date.now() - startTime));
31575
31580
  const verbosity = options?.verbosity;
31576
31581
  if (options?.directOnly === true) {
31577
- return tryDirectPath(inputPath, remaining(), signal, verbosity);
31582
+ return tryDirectPath(inputPath, timeoutMs, signal, verbosity);
31578
31583
  }
31579
- const xmlResult = await tryXmlPath(inputPath, remaining(), signal, buildXmlOpts(options));
31584
+ const xmlResult = await tryXmlPath(inputPath, timeoutMs, signal, buildXmlOpts(options));
31580
31585
  if (xmlResult.ok) {
31581
31586
  return xmlResult;
31582
31587
  }
31583
- return tryDirectPath(inputPath, remaining(), signal, verbosity);
31588
+ return fallbackToDirect(inputPath, xmlResult.error, { budget: remaining(), timeoutMs, signal, verbosity });
31589
+ }
31590
+ async function fallbackToDirect(inputPath, xmlError, context) {
31591
+ if (context.budget === 0) {
31592
+ return err(
31593
+ new Error(
31594
+ `dcm2json: XML path failed and timeout budget (${String(context.timeoutMs)}ms) is exhausted (skipping direct fallback) | xml: ${xmlError.message}`
31595
+ )
31596
+ );
31597
+ }
31598
+ const directResult = await tryDirectPath(inputPath, context.budget, context.signal, context.verbosity);
31599
+ if (directResult.ok) {
31600
+ return directResult;
31601
+ }
31602
+ return err(new Error(`dcm2json: both paths failed | xml: ${xmlError.message} | direct: ${directResult.error.message}`));
31603
+ }
31604
+
31605
+ // src/tools/_charset.ts
31606
+ var LATIN1 = { kind: "latin1" };
31607
+ function label(name) {
31608
+ return { kind: "label", label: name };
31609
+ }
31610
+ var CHARSET_DECODERS = {
31611
+ "ISO_IR 6": LATIN1,
31612
+ "ISO_IR 100": LATIN1,
31613
+ "ISO_IR 101": label("iso-8859-2"),
31614
+ "ISO_IR 109": label("iso-8859-3"),
31615
+ "ISO_IR 110": label("iso-8859-4"),
31616
+ "ISO_IR 144": label("iso-8859-5"),
31617
+ "ISO_IR 127": label("iso-8859-6"),
31618
+ "ISO_IR 126": label("iso-8859-7"),
31619
+ "ISO_IR 138": label("iso-8859-8"),
31620
+ "ISO_IR 148": label("iso-8859-9"),
31621
+ "ISO_IR 203": label("iso-8859-15"),
31622
+ "ISO_IR 13": label("shift_jis"),
31623
+ "ISO_IR 166": label("windows-874"),
31624
+ "ISO_IR 192": label("utf-8"),
31625
+ GB18030: label("gb18030"),
31626
+ GBK: label("gbk")
31627
+ };
31628
+ var ISO2022_INITIAL = {
31629
+ "ISO 2022 IR 6": LATIN1,
31630
+ "ISO 2022 IR 13": label("shift_jis"),
31631
+ "ISO 2022 IR 87": LATIN1,
31632
+ "ISO 2022 IR 159": LATIN1,
31633
+ "ISO 2022 IR 149": label("euc-kr"),
31634
+ "ISO 2022 IR 58": label("gbk"),
31635
+ "ISO 2022 IR 100": LATIN1,
31636
+ "ISO 2022 IR 101": label("iso-8859-2"),
31637
+ "ISO 2022 IR 109": label("iso-8859-3"),
31638
+ "ISO 2022 IR 110": label("iso-8859-4"),
31639
+ "ISO 2022 IR 144": label("iso-8859-5"),
31640
+ "ISO 2022 IR 127": label("iso-8859-6"),
31641
+ "ISO 2022 IR 126": label("iso-8859-7"),
31642
+ "ISO 2022 IR 138": label("iso-8859-8"),
31643
+ "ISO 2022 IR 148": label("iso-8859-9"),
31644
+ "ISO 2022 IR 166": label("windows-874"),
31645
+ "ISO 2022 IR 203": label("iso-8859-15")
31646
+ };
31647
+ var ESCAPE_DECODERS = {
31648
+ "(B": LATIN1,
31649
+ // G0 = ASCII
31650
+ "(J": LATIN1,
31651
+ // G0 = JIS X 0201 romaji
31652
+ ")I": { kind: "katakana" },
31653
+ // G1 = JIS X 0201 katakana
31654
+ "$@": { kind: "iso2022jp", escape: [27, 36, 64] },
31655
+ // G0 = JIS X 0208-1978
31656
+ $B: { kind: "iso2022jp", escape: [27, 36, 66] },
31657
+ // G0 = JIS X 0208
31658
+ "$(D": { kind: "iso2022jp", escape: [27, 36, 40, 68] },
31659
+ // G0 = JIS X 0212
31660
+ "$)C": label("euc-kr"),
31661
+ // G1 = KS X 1001
31662
+ "$)A": label("gbk"),
31663
+ // G1 = GB 2312
31664
+ "-A": LATIN1,
31665
+ // G1 = ISO-8859-1
31666
+ "-B": label("iso-8859-2"),
31667
+ "-C": label("iso-8859-3"),
31668
+ "-D": label("iso-8859-4"),
31669
+ "-F": label("iso-8859-7"),
31670
+ "-G": label("iso-8859-6"),
31671
+ "-H": label("iso-8859-8"),
31672
+ "-L": label("iso-8859-5"),
31673
+ "-M": label("iso-8859-9"),
31674
+ "-T": label("windows-874"),
31675
+ "-b": label("iso-8859-15")
31676
+ };
31677
+ var CHARSET_ALIASES = {
31678
+ ascii: "ISO_IR 6",
31679
+ "latin-1": "ISO_IR 100",
31680
+ latin1: "ISO_IR 100",
31681
+ "iso-8859-1": "ISO_IR 100",
31682
+ "latin-2": "ISO_IR 101",
31683
+ "latin-3": "ISO_IR 109",
31684
+ "latin-4": "ISO_IR 110",
31685
+ "latin-5": "ISO_IR 148",
31686
+ "latin-9": "ISO_IR 203",
31687
+ cyrillic: "ISO_IR 144",
31688
+ arabic: "ISO_IR 127",
31689
+ greek: "ISO_IR 126",
31690
+ hebrew: "ISO_IR 138",
31691
+ thai: "ISO_IR 166",
31692
+ "shift-jis": "ISO_IR 13",
31693
+ shift_jis: "ISO_IR 13",
31694
+ "utf-8": "ISO_IR 192",
31695
+ utf8: "ISO_IR 192",
31696
+ gb18030: "GB18030",
31697
+ gbk: "GBK"
31698
+ };
31699
+ var SINGLE_BYTE_LABELS = /* @__PURE__ */ new Set([
31700
+ "iso-8859-2",
31701
+ "iso-8859-3",
31702
+ "iso-8859-4",
31703
+ "iso-8859-5",
31704
+ "iso-8859-6",
31705
+ "iso-8859-7",
31706
+ "iso-8859-8",
31707
+ "iso-8859-9",
31708
+ "iso-8859-15",
31709
+ "windows-874"
31710
+ ]);
31711
+ function isSingleByteDecoder(decoder) {
31712
+ return decoder.kind === "latin1" || decoder.kind === "label" && SINGLE_BYTE_LABELS.has(decoder.label);
31713
+ }
31714
+ var decoderCache = /* @__PURE__ */ new Map();
31715
+ function getTextDecoder(labelName) {
31716
+ if (decoderCache.has(labelName)) {
31717
+ return decoderCache.get(labelName);
31718
+ }
31719
+ let decoder;
31720
+ try {
31721
+ decoder = new TextDecoder(labelName);
31722
+ } catch {
31723
+ decoder = void 0;
31724
+ }
31725
+ decoderCache.set(labelName, decoder);
31726
+ return decoder;
31727
+ }
31728
+ function decodeLatin1(bytes) {
31729
+ return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("latin1");
31730
+ }
31731
+ function decodeSegment(bytes, decoder) {
31732
+ if (bytes.length === 0) return "";
31733
+ switch (decoder.kind) {
31734
+ case "latin1":
31735
+ return decodeLatin1(bytes);
31736
+ case "label": {
31737
+ const td = getTextDecoder(decoder.label);
31738
+ if (td === void 0) return decodeLatin1(bytes);
31739
+ return td.decode(bytes);
31740
+ }
31741
+ case "iso2022jp": {
31742
+ const td = getTextDecoder("iso-2022-jp");
31743
+ if (td === void 0) return decodeLatin1(bytes);
31744
+ const wrapped = new Uint8Array(decoder.escape.length + bytes.length);
31745
+ wrapped.set(decoder.escape, 0);
31746
+ wrapped.set(bytes, decoder.escape.length);
31747
+ return td.decode(wrapped);
31748
+ }
31749
+ case "katakana": {
31750
+ const td = getTextDecoder("shift_jis");
31751
+ if (td === void 0) return decodeLatin1(bytes);
31752
+ return td.decode(bytes);
31753
+ }
31754
+ }
31755
+ }
31756
+ function readEscape(bytes, start) {
31757
+ let i = start + 1;
31758
+ while (i < bytes.length && bytes[i] >= 32 && bytes[i] <= 47) {
31759
+ i++;
31760
+ }
31761
+ if (i < bytes.length && bytes[i] >= 48 && bytes[i] <= 126) {
31762
+ i++;
31763
+ }
31764
+ const seq = bytes.subarray(start + 1, i);
31765
+ return { key: decodeLatin1(seq), length: i - start };
31766
+ }
31767
+ function decodeIso2022(bytes, initial) {
31768
+ let out = "";
31769
+ let current = initial;
31770
+ let segStart = 0;
31771
+ let i = 0;
31772
+ while (i < bytes.length) {
31773
+ if (bytes[i] !== 27) {
31774
+ i++;
31775
+ continue;
31776
+ }
31777
+ out += decodeSegment(bytes.subarray(segStart, i), current);
31778
+ const esc = readEscape(bytes, i);
31779
+ current = ESCAPE_DECODERS[esc.key] ?? current;
31780
+ i += esc.length;
31781
+ segStart = i;
31782
+ }
31783
+ out += decodeSegment(bytes.subarray(segStart), current);
31784
+ return out;
31785
+ }
31786
+ var DEFAULT_CONTEXT = { terms: ["ISO_IR 6"], iso2022: false, initial: LATIN1, singleByte: true };
31787
+ function normalizeCharsetName(input) {
31788
+ const trimmed = input.trim();
31789
+ if (CHARSET_DECODERS[trimmed] !== void 0 || ISO2022_INITIAL[trimmed] !== void 0) {
31790
+ return trimmed;
31791
+ }
31792
+ return CHARSET_ALIASES[trimmed.toLowerCase()];
31793
+ }
31794
+ function buildContext(terms) {
31795
+ const iso2022 = terms.length > 1 || terms.some((t) => t.startsWith("ISO 2022"));
31796
+ const first = terms[0] ?? "ISO_IR 6";
31797
+ const initial = iso2022 ? ISO2022_INITIAL[first] : CHARSET_DECODERS[first];
31798
+ if (initial === void 0) return void 0;
31799
+ return { terms, iso2022, initial, singleByte: !iso2022 && isSingleByteDecoder(initial) };
31800
+ }
31801
+ function parseSpecificCharacterSet(raw) {
31802
+ const values = raw.split("\\").map((v) => v.trim());
31803
+ return values.map((v, idx) => v === "" && idx === 0 ? "ISO 2022 IR 6" : v).filter((v) => v !== "");
31804
+ }
31805
+ function contextFromTerms(terms, fallback, source) {
31806
+ const context = buildContext(terms);
31807
+ if (context !== void 0) {
31808
+ return ok(context);
31809
+ }
31810
+ if (fallback !== void 0) {
31811
+ const fallbackTerm = normalizeCharsetName(fallback);
31812
+ const fallbackContext = fallbackTerm !== void 0 ? buildContext([fallbackTerm]) : void 0;
31813
+ if (fallbackContext !== void 0) {
31814
+ return ok(fallbackContext);
31815
+ }
31816
+ return err(new Error(`Unsupported charset fallback '${fallback}' (while handling unsupported ${source})`));
31817
+ }
31818
+ return err(new Error(`Unsupported character set: ${source} \u2014 provide charsetFallback (e.g. 'latin-1') to decode best-effort`));
31819
+ }
31820
+ function resolveCharsetContext(specificCharacterSet, charsetAssume, charsetFallback) {
31821
+ if (specificCharacterSet === void 0 || specificCharacterSet.trim() === "") {
31822
+ if (charsetAssume === void 0) {
31823
+ return ok(DEFAULT_CONTEXT);
31824
+ }
31825
+ const term = normalizeCharsetName(charsetAssume);
31826
+ if (term === void 0) {
31827
+ return contextFromTerms([charsetAssume], charsetFallback, `charsetAssume '${charsetAssume}'`);
31828
+ }
31829
+ return contextFromTerms([term], charsetFallback, `charsetAssume '${charsetAssume}'`);
31830
+ }
31831
+ const terms = parseSpecificCharacterSet(specificCharacterSet);
31832
+ return contextFromTerms(terms, charsetFallback, `'${specificCharacterSet}' (0008,0005)`);
31833
+ }
31834
+ function decodeDicomText(bytes, context) {
31835
+ if (context.iso2022) {
31836
+ return decodeIso2022(bytes, context.initial);
31837
+ }
31838
+ return decodeSegment(bytes, context.initial);
31839
+ }
31840
+ var strictUtf8;
31841
+ function isProbableUtf8Mislabel(bytes) {
31842
+ let hasHighByte = false;
31843
+ for (const byte of bytes) {
31844
+ if (byte >= 128) {
31845
+ hasHighByte = true;
31846
+ break;
31847
+ }
31848
+ }
31849
+ if (!hasHighByte) return false;
31850
+ strictUtf8 ?? (strictUtf8 = new TextDecoder("utf-8", { fatal: true }));
31851
+ try {
31852
+ strictUtf8.decode(bytes);
31853
+ return true;
31854
+ } catch {
31855
+ return false;
31856
+ }
31857
+ }
31858
+ var UTF8_DECODER = label("utf-8");
31859
+ function decodeUtf8(bytes) {
31860
+ return decodeSegment(bytes, UTF8_DECODER);
31861
+ }
31862
+
31863
+ // src/tools/_p10ToJson.ts
31864
+ var BINARY_VRS = /* @__PURE__ */ new Set(["OB", "OW", "OD", "OF", "OL", "OV", "UN"]);
31865
+ var CHARSET_VRS = /* @__PURE__ */ new Set(["SH", "LO", "ST", "LT", "UC", "UT", "PN"]);
31866
+ var NO_SPLIT_VRS = /* @__PURE__ */ new Set(["ST", "LT", "UT", "UR"]);
31867
+ var NUMERIC_VR_SIZE = { US: 2, SS: 2, UL: 4, SL: 4, FL: 4, FD: 8, SV: 8, UV: 8 };
31868
+ var MAX_DATASETS = 1e5;
31869
+ function toJsonTag(tag2) {
31870
+ return tag2.slice(1).toUpperCase();
31871
+ }
31872
+ function isEncodingArtifact(tag2) {
31873
+ return tag2.endsWith("0000") && tag2.length === 9 || tag2.startsWith("xfffe");
31874
+ }
31875
+ function resolveVr(element) {
31876
+ const vr = element.vr;
31877
+ if (vr !== void 0 && KNOWN_VR_CODES.has(vr)) {
31878
+ return vr;
31879
+ }
31880
+ return "UN";
31881
+ }
31882
+ function valueBytes(element, byteArray) {
31883
+ return byteArray.subarray(element.dataOffset, element.dataOffset + element.length);
31884
+ }
31885
+ function trimPadding(value) {
31886
+ let end = value.length;
31887
+ while (end > 0) {
31888
+ const ch = value.charCodeAt(end - 1);
31889
+ if (ch !== 32 && ch !== 0) break;
31890
+ end--;
31891
+ }
31892
+ return value.slice(0, end);
31893
+ }
31894
+ function readNumericAt(view, offset, vr, littleEndian) {
31895
+ switch (vr) {
31896
+ case "US":
31897
+ return view.getUint16(offset, littleEndian);
31898
+ case "SS":
31899
+ return view.getInt16(offset, littleEndian);
31900
+ case "UL":
31901
+ return view.getUint32(offset, littleEndian);
31902
+ case "SL":
31903
+ return view.getInt32(offset, littleEndian);
31904
+ case "FL":
31905
+ return view.getFloat32(offset, littleEndian);
31906
+ case "FD":
31907
+ return view.getFloat64(offset, littleEndian);
31908
+ case "SV":
31909
+ return Number(view.getBigInt64(offset, littleEndian));
31910
+ /* v8 ignore next 2 -- 'UV' is the only remaining caller-provided VR */
31911
+ default:
31912
+ return Number(view.getBigUint64(offset, littleEndian));
31913
+ }
31914
+ }
31915
+ function convertNumeric(element, byteArray, vr, littleEndian) {
31916
+ const size = NUMERIC_VR_SIZE[vr];
31917
+ const count = Math.floor(element.length / size);
31918
+ if (count === 0) {
31919
+ return { vr };
31920
+ }
31921
+ const view = new DataView(byteArray.buffer, byteArray.byteOffset + element.dataOffset, element.length);
31922
+ const values = [];
31923
+ for (let i = 0; i < count; i++) {
31924
+ values.push(readNumericAt(view, i * size, vr, littleEndian));
31925
+ }
31926
+ return { vr, Value: values };
31927
+ }
31928
+ function convertAttributeTag(element, byteArray, littleEndian) {
31929
+ const count = Math.floor(element.length / 4);
31930
+ if (count === 0) {
31931
+ return { vr: "AT" };
31932
+ }
31933
+ const view = new DataView(byteArray.buffer, byteArray.byteOffset + element.dataOffset, element.length);
31934
+ const values = [];
31935
+ for (let i = 0; i < count; i++) {
31936
+ const group = view.getUint16(i * 4, littleEndian);
31937
+ const elem = view.getUint16(i * 4 + 2, littleEndian);
31938
+ values.push(group.toString(16).padStart(4, "0").toUpperCase() + elem.toString(16).padStart(4, "0").toUpperCase());
31939
+ }
31940
+ return { vr: "AT", Value: values };
31941
+ }
31942
+ function personNameToJson(value) {
31943
+ const groups = trimPadding(value).split("=");
31944
+ const result = {};
31945
+ const names = ["Alphabetic", "Ideographic", "Phonetic"];
31946
+ for (let i = 0; i < names.length; i++) {
31947
+ const group = (groups[i] ?? "").replace(/\^+$/u, "");
31948
+ const name = names[i];
31949
+ if (group !== "" && name !== void 0) {
31950
+ result[name] = group;
31951
+ }
31952
+ }
31953
+ return result;
31954
+ }
31955
+ function decodeTextChecked(bytes, tag2, settings) {
31956
+ const { charset, mislabel } = settings;
31957
+ if (charset.singleByte && isProbableUtf8Mislabel(bytes)) {
31958
+ if (!mislabel.seen.has(tag2)) {
31959
+ mislabel.seen.add(tag2);
31960
+ const action = mislabel.promote ? "decoded as UTF-8" : `decoded as '${charset.terms.join("\\")}'`;
31961
+ mislabel.warnings.push(`possible UTF-8 mislabel: ${tag2} (${action})`);
31962
+ }
31963
+ if (mislabel.promote) {
31964
+ return decodeUtf8(bytes);
31965
+ }
31966
+ }
31967
+ return decodeDicomText(bytes, charset);
31968
+ }
31969
+ function convertPersonName2(element, byteArray, settings) {
31970
+ const decoded = decodeTextChecked(valueBytes(element, byteArray), toJsonTag(element.tag), settings);
31971
+ const values = decoded.split("\\").map(personNameToJson);
31972
+ return { vr: "PN", Value: values };
31973
+ }
31974
+ function convertString(element, byteArray, vr, settings) {
31975
+ const bytes = valueBytes(element, byteArray);
31976
+ const decoded = CHARSET_VRS.has(vr) ? decodeTextChecked(bytes, toJsonTag(element.tag), settings) : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("latin1");
31977
+ const rawValues = NO_SPLIT_VRS.has(vr) ? [decoded] : decoded.split("\\");
31978
+ const values = rawValues.map((v) => {
31979
+ const trimmed = trimPadding(v);
31980
+ return vr === "DS" || vr === "IS" ? coerceNumeric(trimmed.trim()) : trimmed;
31981
+ });
31982
+ if (values.length === 1 && values[0] === "") {
31983
+ return { vr };
31984
+ }
31985
+ return { vr, Value: values };
31986
+ }
31987
+ function convertLeaf(element, byteArray, vr, settings) {
31988
+ if (element.fragments !== void 0) {
31989
+ return { vr: "OB" };
31990
+ }
31991
+ if (BINARY_VRS.has(vr)) {
31992
+ return { vr };
31993
+ }
31994
+ if (element.length === 0) {
31995
+ return { vr };
31996
+ }
31997
+ if (NUMERIC_VR_SIZE[vr] !== void 0) {
31998
+ return convertNumeric(element, byteArray, vr, settings.littleEndian);
31999
+ }
32000
+ if (vr === "AT") {
32001
+ return convertAttributeTag(element, byteArray, settings.littleEndian);
32002
+ }
32003
+ if (vr === "PN") {
32004
+ return convertPersonName2(element, byteArray, settings);
32005
+ }
32006
+ return convertString(element, byteArray, vr, settings);
32007
+ }
32008
+ function datasetCharset(src, parent, options) {
32009
+ const raw = src.string("x00080005");
32010
+ if (raw === void 0) {
32011
+ return ok(parent);
32012
+ }
32013
+ return resolveCharsetContext(raw, void 0, options?.charsetFallback);
32014
+ }
32015
+ function convertSequence2(element, stack, charset) {
32016
+ const items = element.items ?? [];
32017
+ const values = [];
32018
+ for (const item of items) {
32019
+ const model = {};
32020
+ values.push(model);
32021
+ if (item.dataSet !== void 0) {
32022
+ stack.push({ src: item.dataSet, out: model, parentCharset: charset });
32023
+ }
32024
+ }
32025
+ if (values.length === 0) {
32026
+ return { vr: "SQ" };
32027
+ }
32028
+ return { vr: "SQ", Value: values };
32029
+ }
32030
+ function isSequence(element, vr) {
32031
+ if (element.fragments !== void 0) {
32032
+ return false;
32033
+ }
32034
+ return vr === "SQ" || element.vr === void 0 && element.items !== void 0;
32035
+ }
32036
+ function convertDataset(unit, stack, state) {
32037
+ const charsetResult = datasetCharset(unit.src, unit.parentCharset, state.options);
32038
+ if (!charsetResult.ok) {
32039
+ return err(charsetResult.error);
32040
+ }
32041
+ const charset = charsetResult.value;
32042
+ const keys = Object.keys(unit.src.elements).sort();
32043
+ for (const key of keys) {
32044
+ const element = unit.src.elements[key];
32045
+ if (element === void 0) continue;
32046
+ if (isEncodingArtifact(element.tag)) continue;
32047
+ const tag2 = toJsonTag(element.tag);
32048
+ const vr = resolveVr(element);
32049
+ if (isSequence(element, vr)) {
32050
+ unit.out[tag2] = convertSequence2(element, stack, charset);
32051
+ } else {
32052
+ unit.out[tag2] = convertLeaf(element, unit.src.byteArray, vr, { charset, littleEndian: state.littleEndian, mislabel: state.mislabel });
32053
+ }
32054
+ }
32055
+ return ok(void 0);
32056
+ }
32057
+ function inflateDeflated(byteArray, position) {
32058
+ const inflated = zlib.inflateRawSync(byteArray.subarray(position));
32059
+ const combined = new Uint8Array(position + inflated.byteLength);
32060
+ combined.set(byteArray.subarray(0, position), 0);
32061
+ combined.set(inflated, position);
32062
+ return combined;
32063
+ }
32064
+ function implicitVrLookup(tag2) {
32065
+ return lookupTag(tag2.slice(1))?.vr;
32066
+ }
32067
+ function parseDicomBuffer(buffer, options) {
32068
+ let dataSet;
32069
+ try {
32070
+ dataSet = dicomParser2__default.default.parseDicom(buffer, { vrCallback: implicitVrLookup, inflater: inflateDeflated });
32071
+ } catch (error) {
32072
+ return err(new Error(`Failed to parse DICOM data: ${stderrLib.stderr(error).message}`));
32073
+ }
32074
+ const littleEndian = dataSet.string("x00020010") !== "1.2.840.10008.1.2.2";
32075
+ const rootCharset = resolveCharsetContext(void 0, options?.charsetAssume, options?.charsetFallback);
32076
+ if (!rootCharset.ok) {
32077
+ return err(rootCharset.error);
32078
+ }
32079
+ const data = {};
32080
+ const stack = [{ src: dataSet, out: data, parentCharset: rootCharset.value }];
32081
+ const mislabel = { promote: options?.utf8MislabelPromote === true, warnings: [], seen: /* @__PURE__ */ new Set() };
32082
+ const state = { littleEndian, options, mislabel };
32083
+ let processed = 0;
32084
+ while (stack.length > 0) {
32085
+ processed++;
32086
+ if (processed > MAX_DATASETS) {
32087
+ return err(new Error(`Failed to parse DICOM data: more than ${String(MAX_DATASETS)} nested datasets`));
32088
+ }
32089
+ const unit = stack.pop();
32090
+ if (unit === void 0) break;
32091
+ const converted = convertDataset(unit, stack, state);
32092
+ if (!converted.ok) {
32093
+ return err(converted.error);
32094
+ }
32095
+ }
32096
+ return ok({ data, warnings: [...dataSet.warnings, ...mislabel.warnings] });
32097
+ }
32098
+ var TS_IMPLICIT_LE = "1.2.840.10008.1.2";
32099
+ var TS_EXPLICIT_BE = "1.2.840.10008.1.2.2";
32100
+ var TS_DEFLATED_LE = "1.2.840.10008.1.2.1.99";
32101
+ var BULK_VRS = /* @__PURE__ */ new Set(["OB", "OW", "OD", "OF", "OL", "OV", "UN"]);
32102
+ var LONG_FORM_VRS = /* @__PURE__ */ new Set(["OB", "OW", "OF", "OD", "OL", "OV", "SQ", "UC", "UR", "UT", "UN", "SV", "UV"]);
32103
+ var UNDEFINED_LENGTH = 4294967295;
32104
+ var MAX_HEADER_BYTES = 12;
32105
+ var MAX_ITERATIONS = 1e4;
32106
+ var MAX_FRAGMENT_HOPS = 1e5;
32107
+ var MAX_READ_CALLS = 1e4;
32108
+ async function readRange(handle, position, length) {
32109
+ const buffer = Buffer.alloc(length);
32110
+ let total = 0;
32111
+ for (let i = 0; i < MAX_READ_CALLS && total < length; i++) {
32112
+ const { bytesRead } = await handle.read(buffer, total, length - total, position + total);
32113
+ if (bytesRead === 0) {
32114
+ return err(new Error(`unexpected EOF at offset ${String(position + total)}`));
32115
+ }
32116
+ total += bytesRead;
32117
+ }
32118
+ if (total < length) {
32119
+ return err(new Error(`short read at offset ${String(position)}`));
32120
+ }
32121
+ return ok(buffer);
32122
+ }
32123
+ function checkAbort(state) {
32124
+ if (state.signal?.aborted === true) {
32125
+ return new Error("aborted");
32126
+ }
32127
+ if (Date.now() > state.deadline) {
32128
+ return new Error(`timed out after ${String(state.timeoutMs)}ms`);
32129
+ }
32130
+ return void 0;
32131
+ }
32132
+ var readPart10Header = dicomParser2__default.default.readPart10Header;
32133
+ function probeMeta(head) {
32134
+ let meta;
32135
+ try {
32136
+ meta = readPart10Header(head);
32137
+ } catch {
32138
+ return void 0;
32139
+ }
32140
+ const tsElement = meta.elements["x00020010"];
32141
+ if (meta.position === void 0 || tsElement === void 0) {
32142
+ return void 0;
32143
+ }
32144
+ const transferSyntax = head.subarray(tsElement.dataOffset, tsElement.dataOffset + tsElement.length).toString("latin1").replace(/[\0 ]+$/u, "");
32145
+ return { metaEnd: meta.position, transferSyntax };
32146
+ }
32147
+ function hasPartialDataSet(e) {
32148
+ return typeof e === "object" && e !== null && "dataSet" in e;
32149
+ }
32150
+ function probeParse(assembled) {
32151
+ try {
32152
+ return { dataSet: dicomParser2__default.default.parseDicom(assembled, { vrCallback: implicitVrLookup }), threw: false };
32153
+ } catch (e) {
32154
+ return { dataSet: hasPartialDataSet(e) ? e.dataSet : void 0, threw: true };
32155
+ }
32156
+ }
32157
+ function maxExtentElement(dataSet) {
32158
+ let best;
32159
+ let bestEnd = -1;
32160
+ for (const key of Object.keys(dataSet.elements)) {
32161
+ const element = dataSet.elements[key];
32162
+ if (element === void 0) continue;
32163
+ const end = element.dataOffset + element.length;
32164
+ if (end > bestEnd) {
32165
+ bestEnd = end;
32166
+ best = element;
32167
+ }
32168
+ }
32169
+ return best;
32170
+ }
32171
+ function formatTag(group, elementNum) {
32172
+ return group.toString(16).padStart(4, "0") + elementNum.toString(16).padStart(4, "0");
32173
+ }
32174
+ function parseElementHeader(assembled, position, state) {
32175
+ if (position + MAX_HEADER_BYTES > assembled.length) {
32176
+ return void 0;
32177
+ }
32178
+ if (!state.explicitVr) {
32179
+ const tag3 = formatTag(assembled.readUInt16LE(position), assembled.readUInt16LE(position + 2));
32180
+ const vr2 = implicitVrLookup(`x${tag3}`) ?? "UN";
32181
+ return { tag: tag3, vr: vr2, length: assembled.readUInt32LE(position + 4), headerLen: 8, lengthOffset: 4 };
32182
+ }
32183
+ const tag2 = state.bigEndian ? formatTag(assembled.readUInt16BE(position), assembled.readUInt16BE(position + 2)) : formatTag(assembled.readUInt16LE(position), assembled.readUInt16LE(position + 2));
32184
+ const vr = assembled.subarray(position + 4, position + 6).toString("latin1");
32185
+ if (!/^[A-Z]{2}$/u.test(vr)) {
32186
+ return void 0;
32187
+ }
32188
+ if (LONG_FORM_VRS.has(vr)) {
32189
+ const length2 = state.bigEndian ? assembled.readUInt32BE(position + 8) : assembled.readUInt32LE(position + 8);
32190
+ return { tag: tag2, vr, length: length2, headerLen: 12, lengthOffset: 8 };
32191
+ }
32192
+ const length = state.bigEndian ? assembled.readUInt16BE(position + 6) : assembled.readUInt16LE(position + 6);
32193
+ return { tag: tag2, vr, length, headerLen: 8, lengthOffset: 6 };
32194
+ }
32195
+ function decideAfterThrow(probe, state) {
32196
+ if (state.filePos >= state.fileSize) {
32197
+ return { kind: "fullRead" };
32198
+ }
32199
+ if (probe.dataSet === void 0) {
32200
+ return { kind: "grow" };
32201
+ }
32202
+ const last = maxExtentElement(probe.dataSet);
32203
+ const maxEnd = last === void 0 ? state.metaEnd : last.dataOffset + last.length;
32204
+ if (last !== void 0 && maxEnd > state.assembled.length) {
32205
+ return decideOversized(last, last.hadUndefinedLength === true, state);
32206
+ }
32207
+ const header = parseElementHeader(state.assembled, maxEnd, state);
32208
+ if (header === void 0) {
32209
+ return { kind: "grow" };
32210
+ }
32211
+ return decideWithHeader(state, maxEnd, header, header.length === UNDEFINED_LENGTH);
32212
+ }
32213
+ function decideWithHeader(state, position, header, undefinedLength) {
32214
+ if (undefinedLength) {
32215
+ if (header.tag === "7fe00010" && (header.vr === "OB" || header.vr === "OW") && state.explicitVr && !state.bigEndian) {
32216
+ rewriteHeader(state.assembled, position, header, true);
32217
+ return { kind: "skipEncapsulated", headerStart: position, headerEnd: position + header.headerLen };
32218
+ }
32219
+ return { kind: "grow" };
32220
+ }
32221
+ if (BULK_VRS.has(header.vr)) {
32222
+ return rewriteAndSkip(state, position, header);
32223
+ }
32224
+ return { kind: "grow" };
32225
+ }
32226
+ function rewriteHeader(assembled, position, header, forceObVr) {
32227
+ if (header.lengthOffset === 6) {
32228
+ assembled.writeUInt16LE(0, position + header.lengthOffset);
32229
+ } else {
32230
+ assembled.writeUInt32LE(0, position + header.lengthOffset);
32231
+ }
32232
+ if (forceObVr) {
32233
+ assembled.write("OB", position + 4, "latin1");
32234
+ }
32235
+ }
32236
+ function rewriteAndSkip(state, position, header) {
32237
+ rewriteHeader(state.assembled, position, header, false);
32238
+ return { kind: "skipDefined", headerEnd: position + header.headerLen, valueLength: header.length };
32239
+ }
32240
+ function decideAfterSuccess(probe, state) {
32241
+ if (probe.dataSet === void 0) return { kind: "fullRead" };
32242
+ const last = maxExtentElement(probe.dataSet);
32243
+ const maxEnd = last === void 0 ? state.metaEnd : last.dataOffset + last.length;
32244
+ const undefinedAtEnd = last?.hadUndefinedLength === true && maxEnd >= state.assembled.length;
32245
+ if (state.filePos >= state.fileSize) {
32246
+ return { kind: "done" };
32247
+ }
32248
+ if (maxEnd <= state.assembled.length && !undefinedAtEnd) {
32249
+ return { kind: "grow" };
32250
+ }
32251
+ if (last === void 0) return { kind: "grow" };
32252
+ return decideOversized(last, undefinedAtEnd, state);
32253
+ }
32254
+ function decideOversized(last, undefinedAtEnd, state) {
32255
+ const headerStart = findHeaderStart(state.assembled, last, state);
32256
+ if (headerStart === void 0) {
32257
+ return { kind: "grow" };
32258
+ }
32259
+ const header = parseElementHeader(state.assembled, headerStart, state);
32260
+ if (header === void 0) return { kind: "grow" };
32261
+ if (!undefinedAtEnd && header.length !== last.length) {
32262
+ return { kind: "grow" };
32263
+ }
32264
+ return decideWithHeader(state, headerStart, header, undefinedAtEnd);
32265
+ }
32266
+ function findHeaderStart(assembled, element, state) {
32267
+ const candidates = state.explicitVr ? [12, 8] : [8];
32268
+ for (const headerLen of candidates) {
32269
+ const start = element.dataOffset - headerLen;
32270
+ if (start < 0) continue;
32271
+ const header = parseElementHeader(assembled, start, state);
32272
+ if (header !== void 0 && header.headerLen === headerLen) {
32273
+ return start;
32274
+ }
32275
+ }
32276
+ return void 0;
32277
+ }
32278
+ function fileOffsetOf(state, assembledPos) {
32279
+ return state.filePos - (state.assembled.length - assembledPos);
32280
+ }
32281
+ function applyDefinedSkip(state, headerEnd, valueLength) {
32282
+ const valueStartInFile = fileOffsetOf(state, headerEnd);
32283
+ if (valueStartInFile + valueLength > state.fileSize) {
32284
+ return err(new Error("declared value length extends beyond EOF"));
32285
+ }
32286
+ state.filePos = valueStartInFile + valueLength;
32287
+ state.assembled = state.assembled.subarray(0, headerEnd);
32288
+ state.growSize = state.chunkBytes;
32289
+ return ok(void 0);
32290
+ }
32291
+ async function hopFragments(handle, start, fileSize) {
32292
+ let position = start;
32293
+ for (let i = 0; i < MAX_FRAGMENT_HOPS; i++) {
32294
+ if (position + 8 > fileSize) {
32295
+ return err(new Error("encapsulated data truncated"));
32296
+ }
32297
+ const headerResult = await readRange(handle, position, 8);
32298
+ if (!headerResult.ok) {
32299
+ return err(headerResult.error);
32300
+ }
32301
+ const group = headerResult.value.readUInt16LE(0);
32302
+ const elementNum = headerResult.value.readUInt16LE(2);
32303
+ const itemLength = headerResult.value.readUInt32LE(4);
32304
+ if (group !== 65534) {
32305
+ return err(new Error("unexpected tag between fragments"));
32306
+ }
32307
+ if (elementNum === 57565) {
32308
+ return ok(position + 8);
32309
+ }
32310
+ if (elementNum !== 57344 || itemLength === UNDEFINED_LENGTH) {
32311
+ return err(new Error("malformed fragment item"));
32312
+ }
32313
+ position += 8 + itemLength;
32314
+ }
32315
+ return err(new Error("fragment count exceeded bound"));
32316
+ }
32317
+ async function applyEncapsulatedSkip(state, headerEnd) {
32318
+ const valueStartInFile = fileOffsetOf(state, headerEnd);
32319
+ const afterDelimiter = await hopFragments(state.handle, valueStartInFile, state.fileSize);
32320
+ if (!afterDelimiter.ok) {
32321
+ return err(afterDelimiter.error);
32322
+ }
32323
+ state.filePos = afterDelimiter.value;
32324
+ state.assembled = state.assembled.subarray(0, headerEnd);
32325
+ state.growSize = state.chunkBytes;
32326
+ return ok(void 0);
32327
+ }
32328
+ async function appendChunk(state) {
32329
+ const length = Math.min(state.growSize, state.fileSize - state.filePos);
32330
+ if (length <= 0) return err(new Error("no bytes left to grow"));
32331
+ const chunk = await readRange(state.handle, state.filePos, length);
32332
+ if (!chunk.ok) {
32333
+ return err(chunk.error);
32334
+ }
32335
+ state.assembled = Buffer.concat([state.assembled, chunk.value]);
32336
+ state.filePos += length;
32337
+ state.growSize = Math.min(state.growSize * 2, state.fileSize);
32338
+ return ok(void 0);
32339
+ }
32340
+ async function applyAction(action, state) {
32341
+ switch (action.kind) {
32342
+ case "done":
32343
+ return "done";
32344
+ case "fullRead":
32345
+ return "fullRead";
32346
+ case "grow":
32347
+ return (await appendChunk(state)).ok ? "continue" : "fullRead";
32348
+ case "skipDefined":
32349
+ return applyDefinedSkip(state, action.headerEnd, action.valueLength).ok ? "continue" : "fullRead";
32350
+ case "skipEncapsulated":
32351
+ return (await applyEncapsulatedSkip(state, action.headerEnd)).ok ? "continue" : "fullRead";
32352
+ }
32353
+ }
32354
+ async function boundedLoop(state) {
32355
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
32356
+ const abort = checkAbort(state);
32357
+ if (abort !== void 0) {
32358
+ return err(abort);
32359
+ }
32360
+ const probe = probeParse(state.assembled);
32361
+ const action = probe.threw ? decideAfterThrow(probe, state) : decideAfterSuccess(probe, state);
32362
+ const outcome = await applyAction(action, state);
32363
+ if (outcome === "done") {
32364
+ return ok(state.assembled);
32365
+ }
32366
+ if (outcome === "fullRead") {
32367
+ return "fullRead";
32368
+ }
32369
+ }
32370
+ return "fullRead";
32371
+ }
32372
+ async function readWhole(handle, fileSize, state) {
32373
+ const abort = checkAbort(state);
32374
+ if (abort !== void 0) {
32375
+ return err(abort);
32376
+ }
32377
+ return readRange(handle, 0, fileSize);
32378
+ }
32379
+ async function readWithHandle(handle, options) {
32380
+ const fileSize = (await handle.stat()).size;
32381
+ const deadline = Date.now() + options.timeoutMs;
32382
+ const guard = { deadline, signal: options.signal, timeoutMs: options.timeoutMs };
32383
+ const threshold = options.thresholdBytes ?? BOUNDED_READ_THRESHOLD_BYTES;
32384
+ const chunkBytes = options.chunkBytes ?? BOUNDED_READ_CHUNK_BYTES;
32385
+ if (fileSize <= threshold) {
32386
+ return readWhole(handle, fileSize, guard);
32387
+ }
32388
+ const headResult = await readRange(handle, 0, Math.min(chunkBytes, fileSize));
32389
+ if (!headResult.ok) {
32390
+ return err(headResult.error);
32391
+ }
32392
+ const meta = probeMeta(headResult.value);
32393
+ if (meta === void 0 || meta.transferSyntax === TS_DEFLATED_LE) {
32394
+ return readWhole(handle, fileSize, guard);
32395
+ }
32396
+ const state = {
32397
+ handle,
32398
+ fileSize,
32399
+ chunkBytes,
32400
+ explicitVr: meta.transferSyntax !== TS_IMPLICIT_LE,
32401
+ bigEndian: meta.transferSyntax === TS_EXPLICIT_BE,
32402
+ metaEnd: meta.metaEnd,
32403
+ deadline,
32404
+ timeoutMs: options.timeoutMs,
32405
+ signal: options.signal,
32406
+ assembled: headResult.value,
32407
+ filePos: headResult.value.length,
32408
+ growSize: chunkBytes
32409
+ };
32410
+ const result = await boundedLoop(state);
32411
+ if (result === "fullRead") {
32412
+ return readWhole(handle, fileSize, guard);
32413
+ }
32414
+ return result;
32415
+ }
32416
+ async function readDicomHead(inputPath, options) {
32417
+ let handle;
32418
+ try {
32419
+ handle = await promises$1.open(inputPath, "r");
32420
+ } catch (error) {
32421
+ const message = error instanceof Error ? error.message : String(error);
32422
+ return err(new Error(`failed to read '${inputPath}': ${message}`));
32423
+ }
32424
+ try {
32425
+ const result = await readWithHandle(handle, options);
32426
+ if (result.ok) {
32427
+ return result;
32428
+ }
32429
+ return err(new Error(`reading '${inputPath}' failed: ${result.error.message}`));
32430
+ } catch (error) {
32431
+ const message = error instanceof Error ? error.message : String(error);
32432
+ return err(new Error(`failed to read '${inputPath}': ${message}`));
32433
+ } finally {
32434
+ await handle.close();
32435
+ }
32436
+ }
32437
+
32438
+ // src/tools/dicom2json.ts
32439
+ var Dicom2jsonOptionsSchema = zod.z.object({
32440
+ timeoutMs: zod.z.number().int().positive().optional(),
32441
+ signal: zod.z.instanceof(AbortSignal).optional(),
32442
+ charsetAssume: zod.z.string().min(1).optional(),
32443
+ charsetFallback: zod.z.string().min(1).optional(),
32444
+ utf8MislabelPromote: zod.z.boolean().optional(),
32445
+ dcmtkFallback: zod.z.boolean().optional(),
32446
+ boundedRead: zod.z.boolean().optional()
32447
+ }).strict().optional();
32448
+ var Dicom2jsonBufferOptionsSchema = zod.z.object({
32449
+ charsetAssume: zod.z.string().min(1).optional(),
32450
+ charsetFallback: zod.z.string().min(1).optional(),
32451
+ utf8MislabelPromote: zod.z.boolean().optional()
32452
+ }).strict().optional();
32453
+ async function readFileBounded(inputPath, timeoutMs, signal) {
32454
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
32455
+ const combined = signal !== void 0 ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
32456
+ try {
32457
+ return ok(await promises$1.readFile(inputPath, { signal: combined }));
32458
+ } catch (error) {
32459
+ if (timeoutSignal.aborted) {
32460
+ return err(new Error(`dicom2json: reading '${inputPath}' timed out after ${String(timeoutMs)}ms`));
32461
+ }
32462
+ if (signal?.aborted === true) {
32463
+ return err(new Error(`dicom2json: aborted while reading '${inputPath}'`));
32464
+ }
32465
+ const message = error instanceof Error ? error.message : String(error);
32466
+ return err(new Error(`dicom2json: failed to read '${inputPath}': ${message}`));
32467
+ }
32468
+ }
32469
+ async function runDcmtkFallback(inputPath, jsError, remainingMs, options) {
32470
+ if (remainingMs <= 0) {
32471
+ return err(new Error(`dicom2json: JS parse failed and timeout budget exhausted (skipping DCMTK fallback) | js: ${jsError.message}`));
32472
+ }
32473
+ const fallback = await dcm2json(inputPath, {
32474
+ timeoutMs: remainingMs,
32475
+ signal: options?.signal,
32476
+ charsetAssume: options?.charsetAssume,
32477
+ charsetFallback: options?.charsetFallback
32478
+ });
32479
+ if (!fallback.ok) {
32480
+ return err(new Error(`dicom2json: both engines failed | js: ${jsError.message} | dcmtk: ${fallback.error.message}`));
32481
+ }
32482
+ return ok({ data: fallback.value.data, warnings: [], source: fallback.value.source });
32483
+ }
32484
+ async function dicom2json(inputPath, options) {
32485
+ const validation = Dicom2jsonOptionsSchema.safeParse(options);
32486
+ if (!validation.success) {
32487
+ return err(createValidationError("dicom2json", validation.error));
32488
+ }
32489
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
32490
+ const startTime = Date.now();
32491
+ const parsed = await parseFromFile(inputPath, timeoutMs, options);
32492
+ if (parsed.ok) {
32493
+ return parsed;
32494
+ }
32495
+ if (options?.dcmtkFallback === true) {
32496
+ return runDcmtkFallback(inputPath, parsed.error, timeoutMs - (Date.now() - startTime), options);
32497
+ }
32498
+ return err(parsed.error);
32499
+ }
32500
+ async function readHeadBounded(inputPath, timeoutMs, signal) {
32501
+ const result = await readDicomHead(inputPath, { timeoutMs, signal });
32502
+ if (result.ok) {
32503
+ return result;
32504
+ }
32505
+ return err(new Error(`dicom2json: ${result.error.message}`));
32506
+ }
32507
+ async function parseFromFile(inputPath, timeoutMs, options) {
32508
+ const fileResult = options?.boundedRead === false ? await readFileBounded(inputPath, timeoutMs, options?.signal) : await readHeadBounded(inputPath, timeoutMs, options?.signal);
32509
+ if (!fileResult.ok) {
32510
+ return err(fileResult.error);
32511
+ }
32512
+ const parsed = parseDicomBuffer(fileResult.value, {
32513
+ charsetAssume: options?.charsetAssume,
32514
+ charsetFallback: options?.charsetFallback,
32515
+ utf8MislabelPromote: options?.utf8MislabelPromote
32516
+ });
32517
+ if (!parsed.ok) {
32518
+ return err(parsed.error);
32519
+ }
32520
+ return ok({ data: parsed.value.data, warnings: parsed.value.warnings, source: "js" });
32521
+ }
32522
+ function dicom2jsonFromBuffer(buffer, options) {
32523
+ const validation = Dicom2jsonBufferOptionsSchema.safeParse(options);
32524
+ if (!validation.success) {
32525
+ return err(createValidationError("dicom2jsonFromBuffer", validation.error));
32526
+ }
32527
+ const parsed = parseDicomBuffer(buffer, options);
32528
+ if (!parsed.ok) {
32529
+ return err(parsed.error);
32530
+ }
32531
+ return ok({ data: parsed.value.data, warnings: parsed.value.warnings, source: "js" });
31584
32532
  }
31585
32533
  var DcmdumpFormat = {
31586
32534
  /** Print standard DCMTK format. */
@@ -34393,16 +35341,56 @@ async function unlinkFile(path2) {
34393
35341
  }
34394
35342
 
34395
35343
  // src/dicom/DicomInstance.ts
35344
+ async function readViaDcmtk(path2, shared) {
35345
+ const result = await dcm2json(path2, shared);
35346
+ if (!result.ok) return err(result.error);
35347
+ return ok({ data: result.value.data, warnings: [] });
35348
+ }
35349
+ async function readViaJs(path2, shared, options) {
35350
+ const result = await dicom2json(path2, {
35351
+ ...shared,
35352
+ utf8MislabelPromote: options?.utf8MislabelPromote,
35353
+ boundedRead: options?.boundedRead,
35354
+ dcmtkFallback: true
35355
+ });
35356
+ if (!result.ok) return err(result.error);
35357
+ return ok({ data: result.value.data, warnings: result.value.warnings });
35358
+ }
35359
+ async function readDicomJson(path2, options) {
35360
+ const shared = {
35361
+ timeoutMs: options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
35362
+ signal: options?.signal,
35363
+ charsetAssume: options?.charsetAssume,
35364
+ charsetFallback: options?.charsetFallback
35365
+ };
35366
+ if (options?.engine === "dcmtk") {
35367
+ return readViaDcmtk(path2, shared);
35368
+ }
35369
+ return readViaJs(path2, shared, options);
35370
+ }
34396
35371
  var DicomInstance = class _DicomInstance {
34397
- constructor(dataset, changes, filePath, metadata) {
35372
+ constructor(state) {
34398
35373
  __publicField(this, "dicomDataset");
34399
35374
  __publicField(this, "changeSet");
34400
35375
  __publicField(this, "filepath");
34401
35376
  __publicField(this, "meta");
34402
- this.dicomDataset = dataset;
34403
- this.changeSet = changes;
34404
- this.filepath = filePath;
34405
- this.meta = metadata;
35377
+ __publicField(this, "warningList");
35378
+ this.dicomDataset = state.dataset;
35379
+ this.changeSet = state.changes;
35380
+ this.filepath = state.filePath;
35381
+ this.meta = state.metadata;
35382
+ this.warningList = state.warnings;
35383
+ }
35384
+ /** Creates a copy with the given fields replaced. */
35385
+ derive(overrides) {
35386
+ return new _DicomInstance({
35387
+ dataset: this.dicomDataset,
35388
+ changes: this.changeSet,
35389
+ filePath: this.filepath,
35390
+ metadata: this.meta,
35391
+ warnings: this.warningList,
35392
+ ...overrides
35393
+ });
34406
35394
  }
34407
35395
  // -----------------------------------------------------------------------
34408
35396
  // Factories
@@ -34417,16 +35405,19 @@ var DicomInstance = class _DicomInstance {
34417
35405
  static async open(path2, options) {
34418
35406
  const filePathResult = createDicomFilePath(path2);
34419
35407
  if (!filePathResult.ok) return err(filePathResult.error);
34420
- const jsonResult = await dcm2json(path2, {
34421
- timeoutMs: options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
34422
- signal: options?.signal,
34423
- charsetAssume: options?.charsetAssume,
34424
- charsetFallback: options?.charsetFallback
34425
- });
35408
+ const jsonResult = await readDicomJson(path2, options);
34426
35409
  if (!jsonResult.ok) return err(jsonResult.error);
34427
35410
  const datasetResult = DicomDataset.fromJson(jsonResult.value.data);
34428
35411
  if (!datasetResult.ok) return err(datasetResult.error);
34429
- return ok(new _DicomInstance(datasetResult.value, ChangeSet.empty(), filePathResult.value, /* @__PURE__ */ new Map()));
35412
+ return ok(
35413
+ new _DicomInstance({
35414
+ dataset: datasetResult.value,
35415
+ changes: ChangeSet.empty(),
35416
+ filePath: filePathResult.value,
35417
+ metadata: /* @__PURE__ */ new Map(),
35418
+ warnings: jsonResult.value.warnings
35419
+ })
35420
+ );
34430
35421
  }
34431
35422
  /**
34432
35423
  * Creates a DicomInstance from an existing DicomDataset.
@@ -34442,7 +35433,7 @@ var DicomInstance = class _DicomInstance {
34442
35433
  if (!fpResult.ok) return err(fpResult.error);
34443
35434
  fp = fpResult.value;
34444
35435
  }
34445
- return ok(new _DicomInstance(dataset, ChangeSet.empty(), fp, /* @__PURE__ */ new Map()));
35436
+ return ok(new _DicomInstance({ dataset, changes: ChangeSet.empty(), filePath: fp, metadata: /* @__PURE__ */ new Map(), warnings: [] }));
34446
35437
  }
34447
35438
  // -----------------------------------------------------------------------
34448
35439
  // Read accessors (delegate to DicomDataset)
@@ -34463,6 +35454,15 @@ var DicomInstance = class _DicomInstance {
34463
35454
  get filePath() {
34464
35455
  return this.filepath;
34465
35456
  }
35457
+ /**
35458
+ * Non-fatal parser warnings from opening the file (e.g. `possible UTF-8 mislabel: <tag>`).
35459
+ *
35460
+ * Empty for instances created via {@link DicomInstance.fromDataset} or opened with
35461
+ * `engine: 'dcmtk'`. Preserved across fluent modifications.
35462
+ */
35463
+ get warnings() {
35464
+ return this.warningList;
35465
+ }
34466
35466
  /** Patient's Name (0010,0010). */
34467
35467
  get patientName() {
34468
35468
  return this.dicomDataset.patientName;
@@ -34542,7 +35542,7 @@ var DicomInstance = class _DicomInstance {
34542
35542
  * @param value - The new value
34543
35543
  */
34544
35544
  setTag(path2, value) {
34545
- return new _DicomInstance(this.dicomDataset, this.changeSet.setTag(path2, value), this.filepath, this.meta);
35545
+ return this.derive({ changes: this.changeSet.setTag(path2, value) });
34546
35546
  }
34547
35547
  /**
34548
35548
  * Erases a tag, returning a new DicomInstance.
@@ -34550,11 +35550,11 @@ var DicomInstance = class _DicomInstance {
34550
35550
  * @param path - The DICOM tag path to erase
34551
35551
  */
34552
35552
  eraseTag(path2) {
34553
- return new _DicomInstance(this.dicomDataset, this.changeSet.eraseTag(path2), this.filepath, this.meta);
35553
+ return this.derive({ changes: this.changeSet.eraseTag(path2) });
34554
35554
  }
34555
35555
  /** Erases all private tags, returning a new DicomInstance. */
34556
35556
  erasePrivateTags() {
34557
- return new _DicomInstance(this.dicomDataset, this.changeSet.erasePrivateTags(), this.filepath, this.meta);
35557
+ return this.derive({ changes: this.changeSet.erasePrivateTags() });
34558
35558
  }
34559
35559
  /** Sets Patient's Name (0010,0010). */
34560
35560
  setPatientName(value) {
@@ -34608,7 +35608,7 @@ var DicomInstance = class _DicomInstance {
34608
35608
  * @param entries - A record of tag path → value pairs
34609
35609
  */
34610
35610
  setBatch(entries) {
34611
- return new _DicomInstance(this.dicomDataset, this.changeSet.setBatch(entries), this.filepath, this.meta);
35611
+ return this.derive({ changes: this.changeSet.setBatch(entries) });
34612
35612
  }
34613
35613
  /**
34614
35614
  * Returns a new DicomInstance with the given changes merged into pending changes.
@@ -34617,7 +35617,7 @@ var DicomInstance = class _DicomInstance {
34617
35617
  * @returns A new DicomInstance with accumulated changes
34618
35618
  */
34619
35619
  withChanges(changes) {
34620
- return new _DicomInstance(this.dicomDataset, this.changeSet.merge(changes), this.filepath, this.meta);
35620
+ return this.derive({ changes: this.changeSet.merge(changes) });
34621
35621
  }
34622
35622
  /**
34623
35623
  * Returns a new DicomInstance pointing to a different file path.
@@ -34631,7 +35631,7 @@ var DicomInstance = class _DicomInstance {
34631
35631
  withFilePath(newPath) {
34632
35632
  const result = createDicomFilePath(newPath);
34633
35633
  if (!result.ok) throw result.error;
34634
- return new _DicomInstance(this.dicomDataset, this.changeSet, result.value, this.meta);
35634
+ return this.derive({ filePath: result.value });
34635
35635
  }
34636
35636
  // -----------------------------------------------------------------------
34637
35637
  // File I/O
@@ -34669,7 +35669,7 @@ var DicomInstance = class _DicomInstance {
34669
35669
  return err(applyResult.error);
34670
35670
  }
34671
35671
  }
34672
- return ok(new _DicomInstance(this.dicomDataset, ChangeSet.empty(), outPathResult.value, this.meta));
35672
+ return ok(this.derive({ changes: ChangeSet.empty(), filePath: outPathResult.value }));
34673
35673
  }
34674
35674
  /**
34675
35675
  * Gets the file size in bytes.
@@ -34704,7 +35704,7 @@ var DicomInstance = class _DicomInstance {
34704
35704
  withMetadata(key, value) {
34705
35705
  const newMeta = new Map(this.meta);
34706
35706
  newMeta.set(key, value);
34707
- return new _DicomInstance(this.dicomDataset, this.changeSet, this.filepath, newMeta);
35707
+ return this.derive({ metadata: newMeta });
34708
35708
  }
34709
35709
  /**
34710
35710
  * Gets application metadata by key.
@@ -36511,7 +37511,10 @@ var DicomReceiverOptionsSchema = zod.z.object({
36511
37511
  timeoutMs: zod.z.number().int().positive().optional(),
36512
37512
  signal: zod.z.instanceof(AbortSignal).optional(),
36513
37513
  charsetAssume: zod.z.string().min(1).optional(),
36514
- charsetFallback: zod.z.string().min(1).optional()
37514
+ charsetFallback: zod.z.string().min(1).optional(),
37515
+ utf8MislabelPromote: zod.z.boolean().optional(),
37516
+ boundedRead: zod.z.boolean().optional(),
37517
+ engine: zod.z.enum(["js", "dcmtk"]).optional()
36515
37518
  }).strict().optional(),
36516
37519
  parseInstances: zod.z.boolean().optional(),
36517
37520
  signal: zod.z.instanceof(AbortSignal).optional()
@@ -37952,8 +38955,8 @@ function createStorescuExecutor(options) {
37952
38955
  if (!result.ok) {
37953
38956
  const e = result.error;
37954
38957
  const stdout = e instanceof ToolExecutionError ? e.stdout : "";
37955
- const stderr8 = e instanceof ToolExecutionError ? e.stderr : "";
37956
- return { stdout, stderr: stderr8, error: e };
38958
+ const stderr9 = e instanceof ToolExecutionError ? e.stderr : "";
38959
+ return { stdout, stderr: stderr9, error: e };
37957
38960
  }
37958
38961
  return { stdout: result.value.stdout, stderr: result.value.stderr };
37959
38962
  };
@@ -38210,8 +39213,8 @@ function createDcmsendExecutor(options) {
38210
39213
  if (!result.ok) {
38211
39214
  const e = result.error;
38212
39215
  const stdout = e instanceof ToolExecutionError ? e.stdout : "";
38213
- const stderr8 = e instanceof ToolExecutionError ? e.stderr : "";
38214
- return { stdout, stderr: stderr8, error: e };
39216
+ const stderr9 = e instanceof ToolExecutionError ? e.stderr : "";
39217
+ return { stdout, stderr: stderr9, error: e };
38215
39218
  }
38216
39219
  return { stdout: result.value.stdout, stderr: result.value.stderr };
38217
39220
  };
@@ -38551,7 +39554,7 @@ async function cleanupTempDir(directory) {
38551
39554
  }
38552
39555
  }
38553
39556
  async function parseSingleFile(filePath, timeoutMs, signal) {
38554
- const jsonResult = await dcm2json(filePath, { timeoutMs, signal });
39557
+ const jsonResult = await dicom2json(filePath, { timeoutMs, signal, dcmtkFallback: true });
38555
39558
  if (!jsonResult.ok) {
38556
39559
  return err(jsonResult.error);
38557
39560
  }
@@ -39330,6 +40333,8 @@ exports.dcmscale = dcmscale;
39330
40333
  exports.dcmsend = dcmsend;
39331
40334
  exports.dcod2lum = dcod2lum;
39332
40335
  exports.dconvlum = dconvlum;
40336
+ exports.dicom2json = dicom2json;
40337
+ exports.dicom2jsonFromBuffer = dicom2jsonFromBuffer;
39333
40338
  exports.drtdump = drtdump;
39334
40339
  exports.dsr2xml = dsr2xml;
39335
40340
  exports.dsrdump = dsrdump;