@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/dicom.cjs CHANGED
@@ -9,10 +9,13 @@ var promises = require('fs/promises');
9
9
  var os = require('os');
10
10
  var zod = require('zod');
11
11
  var fastXmlParser = require('fast-xml-parser');
12
+ var zlib = require('zlib');
13
+ var dicomParser2 = require('dicom-parser');
12
14
 
13
15
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
14
16
 
15
17
  var kill__default = /*#__PURE__*/_interopDefault(kill);
18
+ var dicomParser2__default = /*#__PURE__*/_interopDefault(dicomParser2);
16
19
 
17
20
  var __defProp = Object.defineProperty;
18
21
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -29673,6 +29676,8 @@ var REQUIRED_BINARIES = ["dcm2json", "dcm2xml", "dcmodify", "dcmdump", "dcmrecv"
29673
29676
  var MAX_TRAVERSAL_DEPTH = 50;
29674
29677
  var MAX_CHANGESET_OPERATIONS = 1e4;
29675
29678
  var MAX_OUTPUT_BYTES = 100 * 1024 * 1024;
29679
+ var BOUNDED_READ_THRESHOLD_BYTES = 8 * 1024 * 1024;
29680
+ var BOUNDED_READ_CHUNK_BYTES = 1024 * 1024;
29676
29681
 
29677
29682
  // src/dicom/tagPath.ts
29678
29683
  var SEGMENT_PATTERN = /^\(([0-9A-Fa-f]{4}),([0-9A-Fa-f]{4})\)(?:\[(\d+|\*)\])?/;
@@ -30392,9 +30397,9 @@ var ToolExecutionError = class extends Error {
30392
30397
  this.exitCode = details.exitCode;
30393
30398
  }
30394
30399
  };
30395
- function createToolError(toolName, args, exitCode, stderr4, stdout = "") {
30400
+ function createToolError(toolName, args, exitCode, stderr5, stdout = "") {
30396
30401
  const argsStr = truncate(args.join(" "), MAX_ARGS_LENGTH);
30397
- const stderrStr = truncate(stderr4.trim(), MAX_STDERR_LENGTH);
30402
+ const stderrStr = truncate(stderr5.trim(), MAX_STDERR_LENGTH);
30398
30403
  const parts = [`${toolName} failed (exit code ${String(exitCode)})`];
30399
30404
  if (argsStr.length > 0) {
30400
30405
  parts.push(`args: ${argsStr}`);
@@ -30402,7 +30407,7 @@ function createToolError(toolName, args, exitCode, stderr4, stdout = "") {
30402
30407
  if (stderrStr.length > 0) {
30403
30408
  parts.push(`stderr: ${stderrStr}`);
30404
30409
  }
30405
- return new ToolExecutionError(parts.join(" | "), { stdout, stderr: stderr4, exitCode });
30410
+ return new ToolExecutionError(parts.join(" | "), { stdout, stderr: stderr5, exitCode });
30406
30411
  }
30407
30412
  function createValidationError(toolName, zodError) {
30408
30413
  const parts = [];
@@ -30434,7 +30439,7 @@ async function execCommand(binary, args, options) {
30434
30439
  }
30435
30440
  function wireSpawnListeners(binary, child, timeoutMs, resolve) {
30436
30441
  let stdout = "";
30437
- let stderr4 = "";
30442
+ let stderr5 = "";
30438
30443
  let settled = false;
30439
30444
  const name = binary.split("/").pop() ?? binary;
30440
30445
  const settle = (result) => {
@@ -30449,14 +30454,14 @@ function wireSpawnListeners(binary, child, timeoutMs, resolve) {
30449
30454
  }, timeoutMs);
30450
30455
  child.stdout?.on("data", (chunk) => {
30451
30456
  stdout += String(chunk);
30452
- if (stdout.length + stderr4.length > MAX_OUTPUT_BYTES) {
30457
+ if (stdout.length + stderr5.length > MAX_OUTPUT_BYTES) {
30453
30458
  if (child.pid !== void 0 && child.pid !== null) killTree(child.pid);
30454
30459
  settle(err(new Error(`${name} output exceeded ${MAX_OUTPUT_BYTES} bytes`)));
30455
30460
  }
30456
30461
  });
30457
30462
  child.stderr?.on("data", (chunk) => {
30458
- stderr4 += String(chunk);
30459
- if (stdout.length + stderr4.length > MAX_OUTPUT_BYTES) {
30463
+ stderr5 += String(chunk);
30464
+ if (stdout.length + stderr5.length > MAX_OUTPUT_BYTES) {
30460
30465
  if (child.pid !== void 0 && child.pid !== null) killTree(child.pid);
30461
30466
  settle(err(new Error(`${name} output exceeded ${MAX_OUTPUT_BYTES} bytes`)));
30462
30467
  }
@@ -30466,7 +30471,7 @@ function wireSpawnListeners(binary, child, timeoutMs, resolve) {
30466
30471
  settle(err(new Error(`${name} error: ${error.message}`)));
30467
30472
  });
30468
30473
  child.on("close", (code) => {
30469
- settle(ok({ stdout, stderr: stderr4, exitCode: code ?? 1 }));
30474
+ settle(ok({ stdout, stderr: stderr5, exitCode: code ?? 1 }));
30470
30475
  });
30471
30476
  }
30472
30477
  async function spawnCommand(binary, args, options) {
@@ -30688,8 +30693,8 @@ function unwrapValue(v) {
30688
30693
  const obj = v;
30689
30694
  if ("#text" in obj) return obj["#text"];
30690
30695
  const keys = Object.keys(obj);
30691
- if (keys.length === 1 && keys[0] !== void 0 && keys[0].startsWith("@_")) {
30692
- return obj[keys[0]];
30696
+ if (keys.every((k) => k.startsWith("@_"))) {
30697
+ return "";
30693
30698
  }
30694
30699
  return v;
30695
30700
  }
@@ -30895,16 +30900,948 @@ async function dcm2json(inputPath, options) {
30895
30900
  const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
30896
30901
  const signal = options?.signal;
30897
30902
  const startTime = Date.now();
30898
- const remaining = () => Math.max(1e3, timeoutMs - (Date.now() - startTime));
30903
+ const remaining = () => Math.max(0, timeoutMs - (Date.now() - startTime));
30899
30904
  const verbosity = options?.verbosity;
30900
30905
  if (options?.directOnly === true) {
30901
- return tryDirectPath(inputPath, remaining(), signal, verbosity);
30906
+ return tryDirectPath(inputPath, timeoutMs, signal, verbosity);
30902
30907
  }
30903
- const xmlResult = await tryXmlPath(inputPath, remaining(), signal, buildXmlOpts(options));
30908
+ const xmlResult = await tryXmlPath(inputPath, timeoutMs, signal, buildXmlOpts(options));
30904
30909
  if (xmlResult.ok) {
30905
30910
  return xmlResult;
30906
30911
  }
30907
- return tryDirectPath(inputPath, remaining(), signal, verbosity);
30912
+ return fallbackToDirect(inputPath, xmlResult.error, { budget: remaining(), timeoutMs, signal, verbosity });
30913
+ }
30914
+ async function fallbackToDirect(inputPath, xmlError, context) {
30915
+ if (context.budget === 0) {
30916
+ return err(
30917
+ new Error(
30918
+ `dcm2json: XML path failed and timeout budget (${String(context.timeoutMs)}ms) is exhausted (skipping direct fallback) | xml: ${xmlError.message}`
30919
+ )
30920
+ );
30921
+ }
30922
+ const directResult = await tryDirectPath(inputPath, context.budget, context.signal, context.verbosity);
30923
+ if (directResult.ok) {
30924
+ return directResult;
30925
+ }
30926
+ return err(new Error(`dcm2json: both paths failed | xml: ${xmlError.message} | direct: ${directResult.error.message}`));
30927
+ }
30928
+
30929
+ // src/tools/_charset.ts
30930
+ var LATIN1 = { kind: "latin1" };
30931
+ function label(name) {
30932
+ return { kind: "label", label: name };
30933
+ }
30934
+ var CHARSET_DECODERS = {
30935
+ "ISO_IR 6": LATIN1,
30936
+ "ISO_IR 100": LATIN1,
30937
+ "ISO_IR 101": label("iso-8859-2"),
30938
+ "ISO_IR 109": label("iso-8859-3"),
30939
+ "ISO_IR 110": label("iso-8859-4"),
30940
+ "ISO_IR 144": label("iso-8859-5"),
30941
+ "ISO_IR 127": label("iso-8859-6"),
30942
+ "ISO_IR 126": label("iso-8859-7"),
30943
+ "ISO_IR 138": label("iso-8859-8"),
30944
+ "ISO_IR 148": label("iso-8859-9"),
30945
+ "ISO_IR 203": label("iso-8859-15"),
30946
+ "ISO_IR 13": label("shift_jis"),
30947
+ "ISO_IR 166": label("windows-874"),
30948
+ "ISO_IR 192": label("utf-8"),
30949
+ GB18030: label("gb18030"),
30950
+ GBK: label("gbk")
30951
+ };
30952
+ var ISO2022_INITIAL = {
30953
+ "ISO 2022 IR 6": LATIN1,
30954
+ "ISO 2022 IR 13": label("shift_jis"),
30955
+ "ISO 2022 IR 87": LATIN1,
30956
+ "ISO 2022 IR 159": LATIN1,
30957
+ "ISO 2022 IR 149": label("euc-kr"),
30958
+ "ISO 2022 IR 58": label("gbk"),
30959
+ "ISO 2022 IR 100": LATIN1,
30960
+ "ISO 2022 IR 101": label("iso-8859-2"),
30961
+ "ISO 2022 IR 109": label("iso-8859-3"),
30962
+ "ISO 2022 IR 110": label("iso-8859-4"),
30963
+ "ISO 2022 IR 144": label("iso-8859-5"),
30964
+ "ISO 2022 IR 127": label("iso-8859-6"),
30965
+ "ISO 2022 IR 126": label("iso-8859-7"),
30966
+ "ISO 2022 IR 138": label("iso-8859-8"),
30967
+ "ISO 2022 IR 148": label("iso-8859-9"),
30968
+ "ISO 2022 IR 166": label("windows-874"),
30969
+ "ISO 2022 IR 203": label("iso-8859-15")
30970
+ };
30971
+ var ESCAPE_DECODERS = {
30972
+ "(B": LATIN1,
30973
+ // G0 = ASCII
30974
+ "(J": LATIN1,
30975
+ // G0 = JIS X 0201 romaji
30976
+ ")I": { kind: "katakana" },
30977
+ // G1 = JIS X 0201 katakana
30978
+ "$@": { kind: "iso2022jp", escape: [27, 36, 64] },
30979
+ // G0 = JIS X 0208-1978
30980
+ $B: { kind: "iso2022jp", escape: [27, 36, 66] },
30981
+ // G0 = JIS X 0208
30982
+ "$(D": { kind: "iso2022jp", escape: [27, 36, 40, 68] },
30983
+ // G0 = JIS X 0212
30984
+ "$)C": label("euc-kr"),
30985
+ // G1 = KS X 1001
30986
+ "$)A": label("gbk"),
30987
+ // G1 = GB 2312
30988
+ "-A": LATIN1,
30989
+ // G1 = ISO-8859-1
30990
+ "-B": label("iso-8859-2"),
30991
+ "-C": label("iso-8859-3"),
30992
+ "-D": label("iso-8859-4"),
30993
+ "-F": label("iso-8859-7"),
30994
+ "-G": label("iso-8859-6"),
30995
+ "-H": label("iso-8859-8"),
30996
+ "-L": label("iso-8859-5"),
30997
+ "-M": label("iso-8859-9"),
30998
+ "-T": label("windows-874"),
30999
+ "-b": label("iso-8859-15")
31000
+ };
31001
+ var CHARSET_ALIASES = {
31002
+ ascii: "ISO_IR 6",
31003
+ "latin-1": "ISO_IR 100",
31004
+ latin1: "ISO_IR 100",
31005
+ "iso-8859-1": "ISO_IR 100",
31006
+ "latin-2": "ISO_IR 101",
31007
+ "latin-3": "ISO_IR 109",
31008
+ "latin-4": "ISO_IR 110",
31009
+ "latin-5": "ISO_IR 148",
31010
+ "latin-9": "ISO_IR 203",
31011
+ cyrillic: "ISO_IR 144",
31012
+ arabic: "ISO_IR 127",
31013
+ greek: "ISO_IR 126",
31014
+ hebrew: "ISO_IR 138",
31015
+ thai: "ISO_IR 166",
31016
+ "shift-jis": "ISO_IR 13",
31017
+ shift_jis: "ISO_IR 13",
31018
+ "utf-8": "ISO_IR 192",
31019
+ utf8: "ISO_IR 192",
31020
+ gb18030: "GB18030",
31021
+ gbk: "GBK"
31022
+ };
31023
+ var SINGLE_BYTE_LABELS = /* @__PURE__ */ new Set([
31024
+ "iso-8859-2",
31025
+ "iso-8859-3",
31026
+ "iso-8859-4",
31027
+ "iso-8859-5",
31028
+ "iso-8859-6",
31029
+ "iso-8859-7",
31030
+ "iso-8859-8",
31031
+ "iso-8859-9",
31032
+ "iso-8859-15",
31033
+ "windows-874"
31034
+ ]);
31035
+ function isSingleByteDecoder(decoder) {
31036
+ return decoder.kind === "latin1" || decoder.kind === "label" && SINGLE_BYTE_LABELS.has(decoder.label);
31037
+ }
31038
+ var decoderCache = /* @__PURE__ */ new Map();
31039
+ function getTextDecoder(labelName) {
31040
+ if (decoderCache.has(labelName)) {
31041
+ return decoderCache.get(labelName);
31042
+ }
31043
+ let decoder;
31044
+ try {
31045
+ decoder = new TextDecoder(labelName);
31046
+ } catch {
31047
+ decoder = void 0;
31048
+ }
31049
+ decoderCache.set(labelName, decoder);
31050
+ return decoder;
31051
+ }
31052
+ function decodeLatin1(bytes) {
31053
+ return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("latin1");
31054
+ }
31055
+ function decodeSegment(bytes, decoder) {
31056
+ if (bytes.length === 0) return "";
31057
+ switch (decoder.kind) {
31058
+ case "latin1":
31059
+ return decodeLatin1(bytes);
31060
+ case "label": {
31061
+ const td = getTextDecoder(decoder.label);
31062
+ if (td === void 0) return decodeLatin1(bytes);
31063
+ return td.decode(bytes);
31064
+ }
31065
+ case "iso2022jp": {
31066
+ const td = getTextDecoder("iso-2022-jp");
31067
+ if (td === void 0) return decodeLatin1(bytes);
31068
+ const wrapped = new Uint8Array(decoder.escape.length + bytes.length);
31069
+ wrapped.set(decoder.escape, 0);
31070
+ wrapped.set(bytes, decoder.escape.length);
31071
+ return td.decode(wrapped);
31072
+ }
31073
+ case "katakana": {
31074
+ const td = getTextDecoder("shift_jis");
31075
+ if (td === void 0) return decodeLatin1(bytes);
31076
+ return td.decode(bytes);
31077
+ }
31078
+ }
31079
+ }
31080
+ function readEscape(bytes, start) {
31081
+ let i = start + 1;
31082
+ while (i < bytes.length && bytes[i] >= 32 && bytes[i] <= 47) {
31083
+ i++;
31084
+ }
31085
+ if (i < bytes.length && bytes[i] >= 48 && bytes[i] <= 126) {
31086
+ i++;
31087
+ }
31088
+ const seq = bytes.subarray(start + 1, i);
31089
+ return { key: decodeLatin1(seq), length: i - start };
31090
+ }
31091
+ function decodeIso2022(bytes, initial) {
31092
+ let out = "";
31093
+ let current = initial;
31094
+ let segStart = 0;
31095
+ let i = 0;
31096
+ while (i < bytes.length) {
31097
+ if (bytes[i] !== 27) {
31098
+ i++;
31099
+ continue;
31100
+ }
31101
+ out += decodeSegment(bytes.subarray(segStart, i), current);
31102
+ const esc = readEscape(bytes, i);
31103
+ current = ESCAPE_DECODERS[esc.key] ?? current;
31104
+ i += esc.length;
31105
+ segStart = i;
31106
+ }
31107
+ out += decodeSegment(bytes.subarray(segStart), current);
31108
+ return out;
31109
+ }
31110
+ var DEFAULT_CONTEXT = { terms: ["ISO_IR 6"], iso2022: false, initial: LATIN1, singleByte: true };
31111
+ function normalizeCharsetName(input) {
31112
+ const trimmed = input.trim();
31113
+ if (CHARSET_DECODERS[trimmed] !== void 0 || ISO2022_INITIAL[trimmed] !== void 0) {
31114
+ return trimmed;
31115
+ }
31116
+ return CHARSET_ALIASES[trimmed.toLowerCase()];
31117
+ }
31118
+ function buildContext(terms) {
31119
+ const iso2022 = terms.length > 1 || terms.some((t) => t.startsWith("ISO 2022"));
31120
+ const first = terms[0] ?? "ISO_IR 6";
31121
+ const initial = iso2022 ? ISO2022_INITIAL[first] : CHARSET_DECODERS[first];
31122
+ if (initial === void 0) return void 0;
31123
+ return { terms, iso2022, initial, singleByte: !iso2022 && isSingleByteDecoder(initial) };
31124
+ }
31125
+ function parseSpecificCharacterSet(raw) {
31126
+ const values = raw.split("\\").map((v) => v.trim());
31127
+ return values.map((v, idx) => v === "" && idx === 0 ? "ISO 2022 IR 6" : v).filter((v) => v !== "");
31128
+ }
31129
+ function contextFromTerms(terms, fallback, source) {
31130
+ const context = buildContext(terms);
31131
+ if (context !== void 0) {
31132
+ return ok(context);
31133
+ }
31134
+ if (fallback !== void 0) {
31135
+ const fallbackTerm = normalizeCharsetName(fallback);
31136
+ const fallbackContext = fallbackTerm !== void 0 ? buildContext([fallbackTerm]) : void 0;
31137
+ if (fallbackContext !== void 0) {
31138
+ return ok(fallbackContext);
31139
+ }
31140
+ return err(new Error(`Unsupported charset fallback '${fallback}' (while handling unsupported ${source})`));
31141
+ }
31142
+ return err(new Error(`Unsupported character set: ${source} \u2014 provide charsetFallback (e.g. 'latin-1') to decode best-effort`));
31143
+ }
31144
+ function resolveCharsetContext(specificCharacterSet, charsetAssume, charsetFallback) {
31145
+ if (specificCharacterSet === void 0 || specificCharacterSet.trim() === "") {
31146
+ if (charsetAssume === void 0) {
31147
+ return ok(DEFAULT_CONTEXT);
31148
+ }
31149
+ const term = normalizeCharsetName(charsetAssume);
31150
+ if (term === void 0) {
31151
+ return contextFromTerms([charsetAssume], charsetFallback, `charsetAssume '${charsetAssume}'`);
31152
+ }
31153
+ return contextFromTerms([term], charsetFallback, `charsetAssume '${charsetAssume}'`);
31154
+ }
31155
+ const terms = parseSpecificCharacterSet(specificCharacterSet);
31156
+ return contextFromTerms(terms, charsetFallback, `'${specificCharacterSet}' (0008,0005)`);
31157
+ }
31158
+ function decodeDicomText(bytes, context) {
31159
+ if (context.iso2022) {
31160
+ return decodeIso2022(bytes, context.initial);
31161
+ }
31162
+ return decodeSegment(bytes, context.initial);
31163
+ }
31164
+ var strictUtf8;
31165
+ function isProbableUtf8Mislabel(bytes) {
31166
+ let hasHighByte = false;
31167
+ for (const byte of bytes) {
31168
+ if (byte >= 128) {
31169
+ hasHighByte = true;
31170
+ break;
31171
+ }
31172
+ }
31173
+ if (!hasHighByte) return false;
31174
+ strictUtf8 ?? (strictUtf8 = new TextDecoder("utf-8", { fatal: true }));
31175
+ try {
31176
+ strictUtf8.decode(bytes);
31177
+ return true;
31178
+ } catch {
31179
+ return false;
31180
+ }
31181
+ }
31182
+ var UTF8_DECODER = label("utf-8");
31183
+ function decodeUtf8(bytes) {
31184
+ return decodeSegment(bytes, UTF8_DECODER);
31185
+ }
31186
+
31187
+ // src/tools/_p10ToJson.ts
31188
+ var BINARY_VRS = /* @__PURE__ */ new Set(["OB", "OW", "OD", "OF", "OL", "OV", "UN"]);
31189
+ var CHARSET_VRS = /* @__PURE__ */ new Set(["SH", "LO", "ST", "LT", "UC", "UT", "PN"]);
31190
+ var NO_SPLIT_VRS = /* @__PURE__ */ new Set(["ST", "LT", "UT", "UR"]);
31191
+ var NUMERIC_VR_SIZE = { US: 2, SS: 2, UL: 4, SL: 4, FL: 4, FD: 8, SV: 8, UV: 8 };
31192
+ var MAX_DATASETS = 1e5;
31193
+ function toJsonTag(tag) {
31194
+ return tag.slice(1).toUpperCase();
31195
+ }
31196
+ function isEncodingArtifact(tag) {
31197
+ return tag.endsWith("0000") && tag.length === 9 || tag.startsWith("xfffe");
31198
+ }
31199
+ function resolveVr(element) {
31200
+ const vr = element.vr;
31201
+ if (vr !== void 0 && KNOWN_VR_CODES.has(vr)) {
31202
+ return vr;
31203
+ }
31204
+ return "UN";
31205
+ }
31206
+ function valueBytes(element, byteArray) {
31207
+ return byteArray.subarray(element.dataOffset, element.dataOffset + element.length);
31208
+ }
31209
+ function trimPadding(value) {
31210
+ let end = value.length;
31211
+ while (end > 0) {
31212
+ const ch = value.charCodeAt(end - 1);
31213
+ if (ch !== 32 && ch !== 0) break;
31214
+ end--;
31215
+ }
31216
+ return value.slice(0, end);
31217
+ }
31218
+ function readNumericAt(view, offset, vr, littleEndian) {
31219
+ switch (vr) {
31220
+ case "US":
31221
+ return view.getUint16(offset, littleEndian);
31222
+ case "SS":
31223
+ return view.getInt16(offset, littleEndian);
31224
+ case "UL":
31225
+ return view.getUint32(offset, littleEndian);
31226
+ case "SL":
31227
+ return view.getInt32(offset, littleEndian);
31228
+ case "FL":
31229
+ return view.getFloat32(offset, littleEndian);
31230
+ case "FD":
31231
+ return view.getFloat64(offset, littleEndian);
31232
+ case "SV":
31233
+ return Number(view.getBigInt64(offset, littleEndian));
31234
+ /* v8 ignore next 2 -- 'UV' is the only remaining caller-provided VR */
31235
+ default:
31236
+ return Number(view.getBigUint64(offset, littleEndian));
31237
+ }
31238
+ }
31239
+ function convertNumeric(element, byteArray, vr, littleEndian) {
31240
+ const size = NUMERIC_VR_SIZE[vr];
31241
+ const count = Math.floor(element.length / size);
31242
+ if (count === 0) {
31243
+ return { vr };
31244
+ }
31245
+ const view = new DataView(byteArray.buffer, byteArray.byteOffset + element.dataOffset, element.length);
31246
+ const values = [];
31247
+ for (let i = 0; i < count; i++) {
31248
+ values.push(readNumericAt(view, i * size, vr, littleEndian));
31249
+ }
31250
+ return { vr, Value: values };
31251
+ }
31252
+ function convertAttributeTag(element, byteArray, littleEndian) {
31253
+ const count = Math.floor(element.length / 4);
31254
+ if (count === 0) {
31255
+ return { vr: "AT" };
31256
+ }
31257
+ const view = new DataView(byteArray.buffer, byteArray.byteOffset + element.dataOffset, element.length);
31258
+ const values = [];
31259
+ for (let i = 0; i < count; i++) {
31260
+ const group = view.getUint16(i * 4, littleEndian);
31261
+ const elem = view.getUint16(i * 4 + 2, littleEndian);
31262
+ values.push(group.toString(16).padStart(4, "0").toUpperCase() + elem.toString(16).padStart(4, "0").toUpperCase());
31263
+ }
31264
+ return { vr: "AT", Value: values };
31265
+ }
31266
+ function personNameToJson(value) {
31267
+ const groups = trimPadding(value).split("=");
31268
+ const result = {};
31269
+ const names = ["Alphabetic", "Ideographic", "Phonetic"];
31270
+ for (let i = 0; i < names.length; i++) {
31271
+ const group = (groups[i] ?? "").replace(/\^+$/u, "");
31272
+ const name = names[i];
31273
+ if (group !== "" && name !== void 0) {
31274
+ result[name] = group;
31275
+ }
31276
+ }
31277
+ return result;
31278
+ }
31279
+ function decodeTextChecked(bytes, tag, settings) {
31280
+ const { charset, mislabel } = settings;
31281
+ if (charset.singleByte && isProbableUtf8Mislabel(bytes)) {
31282
+ if (!mislabel.seen.has(tag)) {
31283
+ mislabel.seen.add(tag);
31284
+ const action = mislabel.promote ? "decoded as UTF-8" : `decoded as '${charset.terms.join("\\")}'`;
31285
+ mislabel.warnings.push(`possible UTF-8 mislabel: ${tag} (${action})`);
31286
+ }
31287
+ if (mislabel.promote) {
31288
+ return decodeUtf8(bytes);
31289
+ }
31290
+ }
31291
+ return decodeDicomText(bytes, charset);
31292
+ }
31293
+ function convertPersonName2(element, byteArray, settings) {
31294
+ const decoded = decodeTextChecked(valueBytes(element, byteArray), toJsonTag(element.tag), settings);
31295
+ const values = decoded.split("\\").map(personNameToJson);
31296
+ return { vr: "PN", Value: values };
31297
+ }
31298
+ function convertString(element, byteArray, vr, settings) {
31299
+ const bytes = valueBytes(element, byteArray);
31300
+ const decoded = CHARSET_VRS.has(vr) ? decodeTextChecked(bytes, toJsonTag(element.tag), settings) : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("latin1");
31301
+ const rawValues = NO_SPLIT_VRS.has(vr) ? [decoded] : decoded.split("\\");
31302
+ const values = rawValues.map((v) => {
31303
+ const trimmed = trimPadding(v);
31304
+ return vr === "DS" || vr === "IS" ? coerceNumeric(trimmed.trim()) : trimmed;
31305
+ });
31306
+ if (values.length === 1 && values[0] === "") {
31307
+ return { vr };
31308
+ }
31309
+ return { vr, Value: values };
31310
+ }
31311
+ function convertLeaf(element, byteArray, vr, settings) {
31312
+ if (element.fragments !== void 0) {
31313
+ return { vr: "OB" };
31314
+ }
31315
+ if (BINARY_VRS.has(vr)) {
31316
+ return { vr };
31317
+ }
31318
+ if (element.length === 0) {
31319
+ return { vr };
31320
+ }
31321
+ if (NUMERIC_VR_SIZE[vr] !== void 0) {
31322
+ return convertNumeric(element, byteArray, vr, settings.littleEndian);
31323
+ }
31324
+ if (vr === "AT") {
31325
+ return convertAttributeTag(element, byteArray, settings.littleEndian);
31326
+ }
31327
+ if (vr === "PN") {
31328
+ return convertPersonName2(element, byteArray, settings);
31329
+ }
31330
+ return convertString(element, byteArray, vr, settings);
31331
+ }
31332
+ function datasetCharset(src, parent, options) {
31333
+ const raw = src.string("x00080005");
31334
+ if (raw === void 0) {
31335
+ return ok(parent);
31336
+ }
31337
+ return resolveCharsetContext(raw, void 0, options?.charsetFallback);
31338
+ }
31339
+ function convertSequence2(element, stack, charset) {
31340
+ const items = element.items ?? [];
31341
+ const values = [];
31342
+ for (const item of items) {
31343
+ const model = {};
31344
+ values.push(model);
31345
+ if (item.dataSet !== void 0) {
31346
+ stack.push({ src: item.dataSet, out: model, parentCharset: charset });
31347
+ }
31348
+ }
31349
+ if (values.length === 0) {
31350
+ return { vr: "SQ" };
31351
+ }
31352
+ return { vr: "SQ", Value: values };
31353
+ }
31354
+ function isSequence(element, vr) {
31355
+ if (element.fragments !== void 0) {
31356
+ return false;
31357
+ }
31358
+ return vr === "SQ" || element.vr === void 0 && element.items !== void 0;
31359
+ }
31360
+ function convertDataset(unit, stack, state) {
31361
+ const charsetResult = datasetCharset(unit.src, unit.parentCharset, state.options);
31362
+ if (!charsetResult.ok) {
31363
+ return err(charsetResult.error);
31364
+ }
31365
+ const charset = charsetResult.value;
31366
+ const keys = Object.keys(unit.src.elements).sort();
31367
+ for (const key of keys) {
31368
+ const element = unit.src.elements[key];
31369
+ if (element === void 0) continue;
31370
+ if (isEncodingArtifact(element.tag)) continue;
31371
+ const tag = toJsonTag(element.tag);
31372
+ const vr = resolveVr(element);
31373
+ if (isSequence(element, vr)) {
31374
+ unit.out[tag] = convertSequence2(element, stack, charset);
31375
+ } else {
31376
+ unit.out[tag] = convertLeaf(element, unit.src.byteArray, vr, { charset, littleEndian: state.littleEndian, mislabel: state.mislabel });
31377
+ }
31378
+ }
31379
+ return ok(void 0);
31380
+ }
31381
+ function inflateDeflated(byteArray, position) {
31382
+ const inflated = zlib.inflateRawSync(byteArray.subarray(position));
31383
+ const combined = new Uint8Array(position + inflated.byteLength);
31384
+ combined.set(byteArray.subarray(0, position), 0);
31385
+ combined.set(inflated, position);
31386
+ return combined;
31387
+ }
31388
+ function implicitVrLookup(tag) {
31389
+ return lookupTag(tag.slice(1))?.vr;
31390
+ }
31391
+ function parseDicomBuffer(buffer, options) {
31392
+ let dataSet;
31393
+ try {
31394
+ dataSet = dicomParser2__default.default.parseDicom(buffer, { vrCallback: implicitVrLookup, inflater: inflateDeflated });
31395
+ } catch (error) {
31396
+ return err(new Error(`Failed to parse DICOM data: ${stderrLib.stderr(error).message}`));
31397
+ }
31398
+ const littleEndian = dataSet.string("x00020010") !== "1.2.840.10008.1.2.2";
31399
+ const rootCharset = resolveCharsetContext(void 0, options?.charsetAssume, options?.charsetFallback);
31400
+ if (!rootCharset.ok) {
31401
+ return err(rootCharset.error);
31402
+ }
31403
+ const data = {};
31404
+ const stack = [{ src: dataSet, out: data, parentCharset: rootCharset.value }];
31405
+ const mislabel = { promote: options?.utf8MislabelPromote === true, warnings: [], seen: /* @__PURE__ */ new Set() };
31406
+ const state = { littleEndian, options, mislabel };
31407
+ let processed = 0;
31408
+ while (stack.length > 0) {
31409
+ processed++;
31410
+ if (processed > MAX_DATASETS) {
31411
+ return err(new Error(`Failed to parse DICOM data: more than ${String(MAX_DATASETS)} nested datasets`));
31412
+ }
31413
+ const unit = stack.pop();
31414
+ if (unit === void 0) break;
31415
+ const converted = convertDataset(unit, stack, state);
31416
+ if (!converted.ok) {
31417
+ return err(converted.error);
31418
+ }
31419
+ }
31420
+ return ok({ data, warnings: [...dataSet.warnings, ...mislabel.warnings] });
31421
+ }
31422
+ var TS_IMPLICIT_LE = "1.2.840.10008.1.2";
31423
+ var TS_EXPLICIT_BE = "1.2.840.10008.1.2.2";
31424
+ var TS_DEFLATED_LE = "1.2.840.10008.1.2.1.99";
31425
+ var BULK_VRS = /* @__PURE__ */ new Set(["OB", "OW", "OD", "OF", "OL", "OV", "UN"]);
31426
+ var LONG_FORM_VRS = /* @__PURE__ */ new Set(["OB", "OW", "OF", "OD", "OL", "OV", "SQ", "UC", "UR", "UT", "UN", "SV", "UV"]);
31427
+ var UNDEFINED_LENGTH = 4294967295;
31428
+ var MAX_HEADER_BYTES = 12;
31429
+ var MAX_ITERATIONS = 1e4;
31430
+ var MAX_FRAGMENT_HOPS = 1e5;
31431
+ var MAX_READ_CALLS = 1e4;
31432
+ async function readRange(handle, position, length) {
31433
+ const buffer = Buffer.alloc(length);
31434
+ let total = 0;
31435
+ for (let i = 0; i < MAX_READ_CALLS && total < length; i++) {
31436
+ const { bytesRead } = await handle.read(buffer, total, length - total, position + total);
31437
+ if (bytesRead === 0) {
31438
+ return err(new Error(`unexpected EOF at offset ${String(position + total)}`));
31439
+ }
31440
+ total += bytesRead;
31441
+ }
31442
+ if (total < length) {
31443
+ return err(new Error(`short read at offset ${String(position)}`));
31444
+ }
31445
+ return ok(buffer);
31446
+ }
31447
+ function checkAbort(state) {
31448
+ if (state.signal?.aborted === true) {
31449
+ return new Error("aborted");
31450
+ }
31451
+ if (Date.now() > state.deadline) {
31452
+ return new Error(`timed out after ${String(state.timeoutMs)}ms`);
31453
+ }
31454
+ return void 0;
31455
+ }
31456
+ var readPart10Header = dicomParser2__default.default.readPart10Header;
31457
+ function probeMeta(head) {
31458
+ let meta;
31459
+ try {
31460
+ meta = readPart10Header(head);
31461
+ } catch {
31462
+ return void 0;
31463
+ }
31464
+ const tsElement = meta.elements["x00020010"];
31465
+ if (meta.position === void 0 || tsElement === void 0) {
31466
+ return void 0;
31467
+ }
31468
+ const transferSyntax = head.subarray(tsElement.dataOffset, tsElement.dataOffset + tsElement.length).toString("latin1").replace(/[\0 ]+$/u, "");
31469
+ return { metaEnd: meta.position, transferSyntax };
31470
+ }
31471
+ function hasPartialDataSet(e) {
31472
+ return typeof e === "object" && e !== null && "dataSet" in e;
31473
+ }
31474
+ function probeParse(assembled) {
31475
+ try {
31476
+ return { dataSet: dicomParser2__default.default.parseDicom(assembled, { vrCallback: implicitVrLookup }), threw: false };
31477
+ } catch (e) {
31478
+ return { dataSet: hasPartialDataSet(e) ? e.dataSet : void 0, threw: true };
31479
+ }
31480
+ }
31481
+ function maxExtentElement(dataSet) {
31482
+ let best;
31483
+ let bestEnd = -1;
31484
+ for (const key of Object.keys(dataSet.elements)) {
31485
+ const element = dataSet.elements[key];
31486
+ if (element === void 0) continue;
31487
+ const end = element.dataOffset + element.length;
31488
+ if (end > bestEnd) {
31489
+ bestEnd = end;
31490
+ best = element;
31491
+ }
31492
+ }
31493
+ return best;
31494
+ }
31495
+ function formatTag(group, elementNum) {
31496
+ return group.toString(16).padStart(4, "0") + elementNum.toString(16).padStart(4, "0");
31497
+ }
31498
+ function parseElementHeader(assembled, position, state) {
31499
+ if (position + MAX_HEADER_BYTES > assembled.length) {
31500
+ return void 0;
31501
+ }
31502
+ if (!state.explicitVr) {
31503
+ const tag2 = formatTag(assembled.readUInt16LE(position), assembled.readUInt16LE(position + 2));
31504
+ const vr2 = implicitVrLookup(`x${tag2}`) ?? "UN";
31505
+ return { tag: tag2, vr: vr2, length: assembled.readUInt32LE(position + 4), headerLen: 8, lengthOffset: 4 };
31506
+ }
31507
+ const tag = state.bigEndian ? formatTag(assembled.readUInt16BE(position), assembled.readUInt16BE(position + 2)) : formatTag(assembled.readUInt16LE(position), assembled.readUInt16LE(position + 2));
31508
+ const vr = assembled.subarray(position + 4, position + 6).toString("latin1");
31509
+ if (!/^[A-Z]{2}$/u.test(vr)) {
31510
+ return void 0;
31511
+ }
31512
+ if (LONG_FORM_VRS.has(vr)) {
31513
+ const length2 = state.bigEndian ? assembled.readUInt32BE(position + 8) : assembled.readUInt32LE(position + 8);
31514
+ return { tag, vr, length: length2, headerLen: 12, lengthOffset: 8 };
31515
+ }
31516
+ const length = state.bigEndian ? assembled.readUInt16BE(position + 6) : assembled.readUInt16LE(position + 6);
31517
+ return { tag, vr, length, headerLen: 8, lengthOffset: 6 };
31518
+ }
31519
+ function decideAfterThrow(probe, state) {
31520
+ if (state.filePos >= state.fileSize) {
31521
+ return { kind: "fullRead" };
31522
+ }
31523
+ if (probe.dataSet === void 0) {
31524
+ return { kind: "grow" };
31525
+ }
31526
+ const last = maxExtentElement(probe.dataSet);
31527
+ const maxEnd = last === void 0 ? state.metaEnd : last.dataOffset + last.length;
31528
+ if (last !== void 0 && maxEnd > state.assembled.length) {
31529
+ return decideOversized(last, last.hadUndefinedLength === true, state);
31530
+ }
31531
+ const header = parseElementHeader(state.assembled, maxEnd, state);
31532
+ if (header === void 0) {
31533
+ return { kind: "grow" };
31534
+ }
31535
+ return decideWithHeader(state, maxEnd, header, header.length === UNDEFINED_LENGTH);
31536
+ }
31537
+ function decideWithHeader(state, position, header, undefinedLength) {
31538
+ if (undefinedLength) {
31539
+ if (header.tag === "7fe00010" && (header.vr === "OB" || header.vr === "OW") && state.explicitVr && !state.bigEndian) {
31540
+ rewriteHeader(state.assembled, position, header, true);
31541
+ return { kind: "skipEncapsulated", headerStart: position, headerEnd: position + header.headerLen };
31542
+ }
31543
+ return { kind: "grow" };
31544
+ }
31545
+ if (BULK_VRS.has(header.vr)) {
31546
+ return rewriteAndSkip(state, position, header);
31547
+ }
31548
+ return { kind: "grow" };
31549
+ }
31550
+ function rewriteHeader(assembled, position, header, forceObVr) {
31551
+ if (header.lengthOffset === 6) {
31552
+ assembled.writeUInt16LE(0, position + header.lengthOffset);
31553
+ } else {
31554
+ assembled.writeUInt32LE(0, position + header.lengthOffset);
31555
+ }
31556
+ if (forceObVr) {
31557
+ assembled.write("OB", position + 4, "latin1");
31558
+ }
31559
+ }
31560
+ function rewriteAndSkip(state, position, header) {
31561
+ rewriteHeader(state.assembled, position, header, false);
31562
+ return { kind: "skipDefined", headerEnd: position + header.headerLen, valueLength: header.length };
31563
+ }
31564
+ function decideAfterSuccess(probe, state) {
31565
+ if (probe.dataSet === void 0) return { kind: "fullRead" };
31566
+ const last = maxExtentElement(probe.dataSet);
31567
+ const maxEnd = last === void 0 ? state.metaEnd : last.dataOffset + last.length;
31568
+ const undefinedAtEnd = last?.hadUndefinedLength === true && maxEnd >= state.assembled.length;
31569
+ if (state.filePos >= state.fileSize) {
31570
+ return { kind: "done" };
31571
+ }
31572
+ if (maxEnd <= state.assembled.length && !undefinedAtEnd) {
31573
+ return { kind: "grow" };
31574
+ }
31575
+ if (last === void 0) return { kind: "grow" };
31576
+ return decideOversized(last, undefinedAtEnd, state);
31577
+ }
31578
+ function decideOversized(last, undefinedAtEnd, state) {
31579
+ const headerStart = findHeaderStart(state.assembled, last, state);
31580
+ if (headerStart === void 0) {
31581
+ return { kind: "grow" };
31582
+ }
31583
+ const header = parseElementHeader(state.assembled, headerStart, state);
31584
+ if (header === void 0) return { kind: "grow" };
31585
+ if (!undefinedAtEnd && header.length !== last.length) {
31586
+ return { kind: "grow" };
31587
+ }
31588
+ return decideWithHeader(state, headerStart, header, undefinedAtEnd);
31589
+ }
31590
+ function findHeaderStart(assembled, element, state) {
31591
+ const candidates = state.explicitVr ? [12, 8] : [8];
31592
+ for (const headerLen of candidates) {
31593
+ const start = element.dataOffset - headerLen;
31594
+ if (start < 0) continue;
31595
+ const header = parseElementHeader(assembled, start, state);
31596
+ if (header !== void 0 && header.headerLen === headerLen) {
31597
+ return start;
31598
+ }
31599
+ }
31600
+ return void 0;
31601
+ }
31602
+ function fileOffsetOf(state, assembledPos) {
31603
+ return state.filePos - (state.assembled.length - assembledPos);
31604
+ }
31605
+ function applyDefinedSkip(state, headerEnd, valueLength) {
31606
+ const valueStartInFile = fileOffsetOf(state, headerEnd);
31607
+ if (valueStartInFile + valueLength > state.fileSize) {
31608
+ return err(new Error("declared value length extends beyond EOF"));
31609
+ }
31610
+ state.filePos = valueStartInFile + valueLength;
31611
+ state.assembled = state.assembled.subarray(0, headerEnd);
31612
+ state.growSize = state.chunkBytes;
31613
+ return ok(void 0);
31614
+ }
31615
+ async function hopFragments(handle, start, fileSize) {
31616
+ let position = start;
31617
+ for (let i = 0; i < MAX_FRAGMENT_HOPS; i++) {
31618
+ if (position + 8 > fileSize) {
31619
+ return err(new Error("encapsulated data truncated"));
31620
+ }
31621
+ const headerResult = await readRange(handle, position, 8);
31622
+ if (!headerResult.ok) {
31623
+ return err(headerResult.error);
31624
+ }
31625
+ const group = headerResult.value.readUInt16LE(0);
31626
+ const elementNum = headerResult.value.readUInt16LE(2);
31627
+ const itemLength = headerResult.value.readUInt32LE(4);
31628
+ if (group !== 65534) {
31629
+ return err(new Error("unexpected tag between fragments"));
31630
+ }
31631
+ if (elementNum === 57565) {
31632
+ return ok(position + 8);
31633
+ }
31634
+ if (elementNum !== 57344 || itemLength === UNDEFINED_LENGTH) {
31635
+ return err(new Error("malformed fragment item"));
31636
+ }
31637
+ position += 8 + itemLength;
31638
+ }
31639
+ return err(new Error("fragment count exceeded bound"));
31640
+ }
31641
+ async function applyEncapsulatedSkip(state, headerEnd) {
31642
+ const valueStartInFile = fileOffsetOf(state, headerEnd);
31643
+ const afterDelimiter = await hopFragments(state.handle, valueStartInFile, state.fileSize);
31644
+ if (!afterDelimiter.ok) {
31645
+ return err(afterDelimiter.error);
31646
+ }
31647
+ state.filePos = afterDelimiter.value;
31648
+ state.assembled = state.assembled.subarray(0, headerEnd);
31649
+ state.growSize = state.chunkBytes;
31650
+ return ok(void 0);
31651
+ }
31652
+ async function appendChunk(state) {
31653
+ const length = Math.min(state.growSize, state.fileSize - state.filePos);
31654
+ if (length <= 0) return err(new Error("no bytes left to grow"));
31655
+ const chunk = await readRange(state.handle, state.filePos, length);
31656
+ if (!chunk.ok) {
31657
+ return err(chunk.error);
31658
+ }
31659
+ state.assembled = Buffer.concat([state.assembled, chunk.value]);
31660
+ state.filePos += length;
31661
+ state.growSize = Math.min(state.growSize * 2, state.fileSize);
31662
+ return ok(void 0);
31663
+ }
31664
+ async function applyAction(action, state) {
31665
+ switch (action.kind) {
31666
+ case "done":
31667
+ return "done";
31668
+ case "fullRead":
31669
+ return "fullRead";
31670
+ case "grow":
31671
+ return (await appendChunk(state)).ok ? "continue" : "fullRead";
31672
+ case "skipDefined":
31673
+ return applyDefinedSkip(state, action.headerEnd, action.valueLength).ok ? "continue" : "fullRead";
31674
+ case "skipEncapsulated":
31675
+ return (await applyEncapsulatedSkip(state, action.headerEnd)).ok ? "continue" : "fullRead";
31676
+ }
31677
+ }
31678
+ async function boundedLoop(state) {
31679
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
31680
+ const abort = checkAbort(state);
31681
+ if (abort !== void 0) {
31682
+ return err(abort);
31683
+ }
31684
+ const probe = probeParse(state.assembled);
31685
+ const action = probe.threw ? decideAfterThrow(probe, state) : decideAfterSuccess(probe, state);
31686
+ const outcome = await applyAction(action, state);
31687
+ if (outcome === "done") {
31688
+ return ok(state.assembled);
31689
+ }
31690
+ if (outcome === "fullRead") {
31691
+ return "fullRead";
31692
+ }
31693
+ }
31694
+ return "fullRead";
31695
+ }
31696
+ async function readWhole(handle, fileSize, state) {
31697
+ const abort = checkAbort(state);
31698
+ if (abort !== void 0) {
31699
+ return err(abort);
31700
+ }
31701
+ return readRange(handle, 0, fileSize);
31702
+ }
31703
+ async function readWithHandle(handle, options) {
31704
+ const fileSize = (await handle.stat()).size;
31705
+ const deadline = Date.now() + options.timeoutMs;
31706
+ const guard = { deadline, signal: options.signal, timeoutMs: options.timeoutMs };
31707
+ const threshold = options.thresholdBytes ?? BOUNDED_READ_THRESHOLD_BYTES;
31708
+ const chunkBytes = options.chunkBytes ?? BOUNDED_READ_CHUNK_BYTES;
31709
+ if (fileSize <= threshold) {
31710
+ return readWhole(handle, fileSize, guard);
31711
+ }
31712
+ const headResult = await readRange(handle, 0, Math.min(chunkBytes, fileSize));
31713
+ if (!headResult.ok) {
31714
+ return err(headResult.error);
31715
+ }
31716
+ const meta = probeMeta(headResult.value);
31717
+ if (meta === void 0 || meta.transferSyntax === TS_DEFLATED_LE) {
31718
+ return readWhole(handle, fileSize, guard);
31719
+ }
31720
+ const state = {
31721
+ handle,
31722
+ fileSize,
31723
+ chunkBytes,
31724
+ explicitVr: meta.transferSyntax !== TS_IMPLICIT_LE,
31725
+ bigEndian: meta.transferSyntax === TS_EXPLICIT_BE,
31726
+ metaEnd: meta.metaEnd,
31727
+ deadline,
31728
+ timeoutMs: options.timeoutMs,
31729
+ signal: options.signal,
31730
+ assembled: headResult.value,
31731
+ filePos: headResult.value.length,
31732
+ growSize: chunkBytes
31733
+ };
31734
+ const result = await boundedLoop(state);
31735
+ if (result === "fullRead") {
31736
+ return readWhole(handle, fileSize, guard);
31737
+ }
31738
+ return result;
31739
+ }
31740
+ async function readDicomHead(inputPath, options) {
31741
+ let handle;
31742
+ try {
31743
+ handle = await promises.open(inputPath, "r");
31744
+ } catch (error) {
31745
+ const message = error instanceof Error ? error.message : String(error);
31746
+ return err(new Error(`failed to read '${inputPath}': ${message}`));
31747
+ }
31748
+ try {
31749
+ const result = await readWithHandle(handle, options);
31750
+ if (result.ok) {
31751
+ return result;
31752
+ }
31753
+ return err(new Error(`reading '${inputPath}' failed: ${result.error.message}`));
31754
+ } catch (error) {
31755
+ const message = error instanceof Error ? error.message : String(error);
31756
+ return err(new Error(`failed to read '${inputPath}': ${message}`));
31757
+ } finally {
31758
+ await handle.close();
31759
+ }
31760
+ }
31761
+
31762
+ // src/tools/dicom2json.ts
31763
+ var Dicom2jsonOptionsSchema = zod.z.object({
31764
+ timeoutMs: zod.z.number().int().positive().optional(),
31765
+ signal: zod.z.instanceof(AbortSignal).optional(),
31766
+ charsetAssume: zod.z.string().min(1).optional(),
31767
+ charsetFallback: zod.z.string().min(1).optional(),
31768
+ utf8MislabelPromote: zod.z.boolean().optional(),
31769
+ dcmtkFallback: zod.z.boolean().optional(),
31770
+ boundedRead: zod.z.boolean().optional()
31771
+ }).strict().optional();
31772
+ zod.z.object({
31773
+ charsetAssume: zod.z.string().min(1).optional(),
31774
+ charsetFallback: zod.z.string().min(1).optional(),
31775
+ utf8MislabelPromote: zod.z.boolean().optional()
31776
+ }).strict().optional();
31777
+ async function readFileBounded(inputPath, timeoutMs, signal) {
31778
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
31779
+ const combined = signal !== void 0 ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
31780
+ try {
31781
+ return ok(await promises.readFile(inputPath, { signal: combined }));
31782
+ } catch (error) {
31783
+ if (timeoutSignal.aborted) {
31784
+ return err(new Error(`dicom2json: reading '${inputPath}' timed out after ${String(timeoutMs)}ms`));
31785
+ }
31786
+ if (signal?.aborted === true) {
31787
+ return err(new Error(`dicom2json: aborted while reading '${inputPath}'`));
31788
+ }
31789
+ const message = error instanceof Error ? error.message : String(error);
31790
+ return err(new Error(`dicom2json: failed to read '${inputPath}': ${message}`));
31791
+ }
31792
+ }
31793
+ async function runDcmtkFallback(inputPath, jsError, remainingMs, options) {
31794
+ if (remainingMs <= 0) {
31795
+ return err(new Error(`dicom2json: JS parse failed and timeout budget exhausted (skipping DCMTK fallback) | js: ${jsError.message}`));
31796
+ }
31797
+ const fallback = await dcm2json(inputPath, {
31798
+ timeoutMs: remainingMs,
31799
+ signal: options?.signal,
31800
+ charsetAssume: options?.charsetAssume,
31801
+ charsetFallback: options?.charsetFallback
31802
+ });
31803
+ if (!fallback.ok) {
31804
+ return err(new Error(`dicom2json: both engines failed | js: ${jsError.message} | dcmtk: ${fallback.error.message}`));
31805
+ }
31806
+ return ok({ data: fallback.value.data, warnings: [], source: fallback.value.source });
31807
+ }
31808
+ async function dicom2json(inputPath, options) {
31809
+ const validation = Dicom2jsonOptionsSchema.safeParse(options);
31810
+ if (!validation.success) {
31811
+ return err(createValidationError("dicom2json", validation.error));
31812
+ }
31813
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
31814
+ const startTime = Date.now();
31815
+ const parsed = await parseFromFile(inputPath, timeoutMs, options);
31816
+ if (parsed.ok) {
31817
+ return parsed;
31818
+ }
31819
+ if (options?.dcmtkFallback === true) {
31820
+ return runDcmtkFallback(inputPath, parsed.error, timeoutMs - (Date.now() - startTime), options);
31821
+ }
31822
+ return err(parsed.error);
31823
+ }
31824
+ async function readHeadBounded(inputPath, timeoutMs, signal) {
31825
+ const result = await readDicomHead(inputPath, { timeoutMs, signal });
31826
+ if (result.ok) {
31827
+ return result;
31828
+ }
31829
+ return err(new Error(`dicom2json: ${result.error.message}`));
31830
+ }
31831
+ async function parseFromFile(inputPath, timeoutMs, options) {
31832
+ const fileResult = options?.boundedRead === false ? await readFileBounded(inputPath, timeoutMs, options?.signal) : await readHeadBounded(inputPath, timeoutMs, options?.signal);
31833
+ if (!fileResult.ok) {
31834
+ return err(fileResult.error);
31835
+ }
31836
+ const parsed = parseDicomBuffer(fileResult.value, {
31837
+ charsetAssume: options?.charsetAssume,
31838
+ charsetFallback: options?.charsetFallback,
31839
+ utf8MislabelPromote: options?.utf8MislabelPromote
31840
+ });
31841
+ if (!parsed.ok) {
31842
+ return err(parsed.error);
31843
+ }
31844
+ return ok({ data: parsed.value.data, warnings: parsed.value.warnings, source: "js" });
30908
31845
  }
30909
31846
  var TAG_OR_PATH_PATTERN = /^\([0-9A-Fa-f]{4},[0-9A-Fa-f]{4}\)(\[\d+\]\.\([0-9A-Fa-f]{4},[0-9A-Fa-f]{4}\))*(\[\d+\])?$/;
30910
31847
  var TagModificationSchema = zod.z.object({
@@ -31011,16 +31948,56 @@ async function unlinkFile(path) {
31011
31948
  }
31012
31949
 
31013
31950
  // src/dicom/DicomInstance.ts
31951
+ async function readViaDcmtk(path, shared) {
31952
+ const result = await dcm2json(path, shared);
31953
+ if (!result.ok) return err(result.error);
31954
+ return ok({ data: result.value.data, warnings: [] });
31955
+ }
31956
+ async function readViaJs(path, shared, options) {
31957
+ const result = await dicom2json(path, {
31958
+ ...shared,
31959
+ utf8MislabelPromote: options?.utf8MislabelPromote,
31960
+ boundedRead: options?.boundedRead,
31961
+ dcmtkFallback: true
31962
+ });
31963
+ if (!result.ok) return err(result.error);
31964
+ return ok({ data: result.value.data, warnings: result.value.warnings });
31965
+ }
31966
+ async function readDicomJson(path, options) {
31967
+ const shared = {
31968
+ timeoutMs: options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
31969
+ signal: options?.signal,
31970
+ charsetAssume: options?.charsetAssume,
31971
+ charsetFallback: options?.charsetFallback
31972
+ };
31973
+ if (options?.engine === "dcmtk") {
31974
+ return readViaDcmtk(path, shared);
31975
+ }
31976
+ return readViaJs(path, shared, options);
31977
+ }
31014
31978
  var DicomInstance = class _DicomInstance {
31015
- constructor(dataset, changes, filePath, metadata) {
31979
+ constructor(state) {
31016
31980
  __publicField(this, "dicomDataset");
31017
31981
  __publicField(this, "changeSet");
31018
31982
  __publicField(this, "filepath");
31019
31983
  __publicField(this, "meta");
31020
- this.dicomDataset = dataset;
31021
- this.changeSet = changes;
31022
- this.filepath = filePath;
31023
- this.meta = metadata;
31984
+ __publicField(this, "warningList");
31985
+ this.dicomDataset = state.dataset;
31986
+ this.changeSet = state.changes;
31987
+ this.filepath = state.filePath;
31988
+ this.meta = state.metadata;
31989
+ this.warningList = state.warnings;
31990
+ }
31991
+ /** Creates a copy with the given fields replaced. */
31992
+ derive(overrides) {
31993
+ return new _DicomInstance({
31994
+ dataset: this.dicomDataset,
31995
+ changes: this.changeSet,
31996
+ filePath: this.filepath,
31997
+ metadata: this.meta,
31998
+ warnings: this.warningList,
31999
+ ...overrides
32000
+ });
31024
32001
  }
31025
32002
  // -----------------------------------------------------------------------
31026
32003
  // Factories
@@ -31035,16 +32012,19 @@ var DicomInstance = class _DicomInstance {
31035
32012
  static async open(path, options) {
31036
32013
  const filePathResult = createDicomFilePath(path);
31037
32014
  if (!filePathResult.ok) return err(filePathResult.error);
31038
- const jsonResult = await dcm2json(path, {
31039
- timeoutMs: options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
31040
- signal: options?.signal,
31041
- charsetAssume: options?.charsetAssume,
31042
- charsetFallback: options?.charsetFallback
31043
- });
32015
+ const jsonResult = await readDicomJson(path, options);
31044
32016
  if (!jsonResult.ok) return err(jsonResult.error);
31045
32017
  const datasetResult = DicomDataset.fromJson(jsonResult.value.data);
31046
32018
  if (!datasetResult.ok) return err(datasetResult.error);
31047
- return ok(new _DicomInstance(datasetResult.value, ChangeSet.empty(), filePathResult.value, /* @__PURE__ */ new Map()));
32019
+ return ok(
32020
+ new _DicomInstance({
32021
+ dataset: datasetResult.value,
32022
+ changes: ChangeSet.empty(),
32023
+ filePath: filePathResult.value,
32024
+ metadata: /* @__PURE__ */ new Map(),
32025
+ warnings: jsonResult.value.warnings
32026
+ })
32027
+ );
31048
32028
  }
31049
32029
  /**
31050
32030
  * Creates a DicomInstance from an existing DicomDataset.
@@ -31060,7 +32040,7 @@ var DicomInstance = class _DicomInstance {
31060
32040
  if (!fpResult.ok) return err(fpResult.error);
31061
32041
  fp = fpResult.value;
31062
32042
  }
31063
- return ok(new _DicomInstance(dataset, ChangeSet.empty(), fp, /* @__PURE__ */ new Map()));
32043
+ return ok(new _DicomInstance({ dataset, changes: ChangeSet.empty(), filePath: fp, metadata: /* @__PURE__ */ new Map(), warnings: [] }));
31064
32044
  }
31065
32045
  // -----------------------------------------------------------------------
31066
32046
  // Read accessors (delegate to DicomDataset)
@@ -31081,6 +32061,15 @@ var DicomInstance = class _DicomInstance {
31081
32061
  get filePath() {
31082
32062
  return this.filepath;
31083
32063
  }
32064
+ /**
32065
+ * Non-fatal parser warnings from opening the file (e.g. `possible UTF-8 mislabel: <tag>`).
32066
+ *
32067
+ * Empty for instances created via {@link DicomInstance.fromDataset} or opened with
32068
+ * `engine: 'dcmtk'`. Preserved across fluent modifications.
32069
+ */
32070
+ get warnings() {
32071
+ return this.warningList;
32072
+ }
31084
32073
  /** Patient's Name (0010,0010). */
31085
32074
  get patientName() {
31086
32075
  return this.dicomDataset.patientName;
@@ -31160,7 +32149,7 @@ var DicomInstance = class _DicomInstance {
31160
32149
  * @param value - The new value
31161
32150
  */
31162
32151
  setTag(path, value) {
31163
- return new _DicomInstance(this.dicomDataset, this.changeSet.setTag(path, value), this.filepath, this.meta);
32152
+ return this.derive({ changes: this.changeSet.setTag(path, value) });
31164
32153
  }
31165
32154
  /**
31166
32155
  * Erases a tag, returning a new DicomInstance.
@@ -31168,11 +32157,11 @@ var DicomInstance = class _DicomInstance {
31168
32157
  * @param path - The DICOM tag path to erase
31169
32158
  */
31170
32159
  eraseTag(path) {
31171
- return new _DicomInstance(this.dicomDataset, this.changeSet.eraseTag(path), this.filepath, this.meta);
32160
+ return this.derive({ changes: this.changeSet.eraseTag(path) });
31172
32161
  }
31173
32162
  /** Erases all private tags, returning a new DicomInstance. */
31174
32163
  erasePrivateTags() {
31175
- return new _DicomInstance(this.dicomDataset, this.changeSet.erasePrivateTags(), this.filepath, this.meta);
32164
+ return this.derive({ changes: this.changeSet.erasePrivateTags() });
31176
32165
  }
31177
32166
  /** Sets Patient's Name (0010,0010). */
31178
32167
  setPatientName(value) {
@@ -31226,7 +32215,7 @@ var DicomInstance = class _DicomInstance {
31226
32215
  * @param entries - A record of tag path → value pairs
31227
32216
  */
31228
32217
  setBatch(entries) {
31229
- return new _DicomInstance(this.dicomDataset, this.changeSet.setBatch(entries), this.filepath, this.meta);
32218
+ return this.derive({ changes: this.changeSet.setBatch(entries) });
31230
32219
  }
31231
32220
  /**
31232
32221
  * Returns a new DicomInstance with the given changes merged into pending changes.
@@ -31235,7 +32224,7 @@ var DicomInstance = class _DicomInstance {
31235
32224
  * @returns A new DicomInstance with accumulated changes
31236
32225
  */
31237
32226
  withChanges(changes) {
31238
- return new _DicomInstance(this.dicomDataset, this.changeSet.merge(changes), this.filepath, this.meta);
32227
+ return this.derive({ changes: this.changeSet.merge(changes) });
31239
32228
  }
31240
32229
  /**
31241
32230
  * Returns a new DicomInstance pointing to a different file path.
@@ -31249,7 +32238,7 @@ var DicomInstance = class _DicomInstance {
31249
32238
  withFilePath(newPath) {
31250
32239
  const result = createDicomFilePath(newPath);
31251
32240
  if (!result.ok) throw result.error;
31252
- return new _DicomInstance(this.dicomDataset, this.changeSet, result.value, this.meta);
32241
+ return this.derive({ filePath: result.value });
31253
32242
  }
31254
32243
  // -----------------------------------------------------------------------
31255
32244
  // File I/O
@@ -31287,7 +32276,7 @@ var DicomInstance = class _DicomInstance {
31287
32276
  return err(applyResult.error);
31288
32277
  }
31289
32278
  }
31290
- return ok(new _DicomInstance(this.dicomDataset, ChangeSet.empty(), outPathResult.value, this.meta));
32279
+ return ok(this.derive({ changes: ChangeSet.empty(), filePath: outPathResult.value }));
31291
32280
  }
31292
32281
  /**
31293
32282
  * Gets the file size in bytes.
@@ -31322,7 +32311,7 @@ var DicomInstance = class _DicomInstance {
31322
32311
  withMetadata(key, value) {
31323
32312
  const newMeta = new Map(this.meta);
31324
32313
  newMeta.set(key, value);
31325
- return new _DicomInstance(this.dicomDataset, this.changeSet, this.filepath, newMeta);
32314
+ return this.derive({ metadata: newMeta });
31326
32315
  }
31327
32316
  /**
31328
32317
  * Gets application metadata by key.