@cosyte/dicom 0.1.5 → 0.1.7

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.d.cts CHANGED
@@ -2794,7 +2794,11 @@ declare class DicomValueError extends Error {
2794
2794
  * carries an offset that is not zero and names no Directory Record the
2795
2795
  * `Dataset` holds, or is not one 32-bit unsigned integer (never written stale
2796
2796
  * or as zero), or `DIRECTORY_OFFSET_DEFLATED` when a DICOMDIR under Deflated
2797
- * Explicit VR LE carries a non-zero offset. Nothing is returned on a throw.
2797
+ * Explicit VR LE carries a non-zero offset, or `VALUE_TOO_LONG_FOR_VR` when a
2798
+ * Private Creator Data Element under an Explicit VR syntax, or a File Meta
2799
+ * Information element under any, has a value past 65,534 bytes in a VR with a
2800
+ * 16-bit Value Length Field (PS3.5 2026c section 6.2.2 forbids the `UN` such a
2801
+ * value is otherwise written as). Nothing is returned on a throw.
2798
2802
  *
2799
2803
  * @example
2800
2804
  * ```ts
@@ -2849,6 +2853,13 @@ declare function serializeDicom(ds: Dataset): Buffer;
2849
2853
  * not one 32-bit unsigned integer). A position inside a deflated stream
2850
2854
  * names no Item a reader can seek to, so no offset can be written for it.
2851
2855
  * A Deflated DICOMDIR whose offsets are all zero is written.
2856
+ * - `VALUE_TOO_LONG_FOR_VR` - a Private Creator Data Element `(gggg,00xx)`
2857
+ * under an Explicit VR Transfer Syntax, or a File Meta Information element
2858
+ * `(0002,xxxx)` under any, carries a value longer than 65,534 bytes in a VR
2859
+ * whose Explicit VR Value Length Field is 16 bits. PS3.5 2026c section 6.2.2
2860
+ * has such a value written with VR `UN`, which the writer does for every
2861
+ * other element, but says the `UN` VR "shall not be used" for these two, so
2862
+ * the value has no encoding and nothing is written.
2852
2863
  *
2853
2864
  * The message is built only from structural constants (never a decoded
2854
2865
  * attribute value, a Transfer Syntax UID, or a length or byte read from the
@@ -2872,6 +2883,7 @@ declare const SERIALIZE_ERROR_CODES: {
2872
2883
  readonly INVALID_ENCAPSULATED_PIXEL_DATA: "INVALID_ENCAPSULATED_PIXEL_DATA";
2873
2884
  readonly DIRECTORY_OFFSET_UNRESOLVED: "DIRECTORY_OFFSET_UNRESOLVED";
2874
2885
  readonly DIRECTORY_OFFSET_DEFLATED: "DIRECTORY_OFFSET_DEFLATED";
2886
+ readonly VALUE_TOO_LONG_FOR_VR: "VALUE_TOO_LONG_FOR_VR";
2875
2887
  };
2876
2888
  /**
2877
2889
  * Discriminant for {@link DicomSerializeError.code}, enabling exhaustive
@@ -2892,6 +2904,8 @@ declare const SERIALIZE_ERROR_CODES: {
2892
2904
  * return "a DICOMDIR offset names no Directory Record the dataset holds";
2893
2905
  * case "DIRECTORY_OFFSET_DEFLATED":
2894
2906
  * return "a Deflated DICOMDIR carries a non-zero Directory Record offset";
2907
+ * case "VALUE_TOO_LONG_FOR_VR":
2908
+ * return "a Private Creator or File Meta value is too long for its VR";
2895
2909
  * }
2896
2910
  * }
2897
2911
  * ```
@@ -2920,6 +2934,9 @@ type SerializeErrorCode = (typeof SERIALIZE_ERROR_CODES)[keyof typeof SERIALIZE_
2920
2934
  * if (err instanceof DicomSerializeError && err.code === "DIRECTORY_OFFSET_UNRESOLVED") {
2921
2935
  * // a DICOMDIR offset the writer cannot tie to a Directory Record
2922
2936
  * }
2937
+ * if (err instanceof DicomSerializeError && err.code === "VALUE_TOO_LONG_FOR_VR") {
2938
+ * // a Private Creator or File Meta value past 65,534 bytes, which UN may not carry
2939
+ * }
2923
2940
  * }
2924
2941
  * ```
2925
2942
  */
@@ -4679,6 +4696,13 @@ declare class DeidentifyError extends Error {
4679
4696
  * @throws {@link DeidentifyError} (`INVALID_OPTIONS`) for an unknown Retain option,
4680
4697
  * for both PS3.15 §E.3.6 temporal Options in one call, or for a malformed
4681
4698
  * `uidRoot`.
4699
+ * @throws `DicomSerializeError` (`VALUE_TOO_LONG_FOR_VR`) when the run
4700
+ * re-encodes a Sequence Item, under an Explicit VR syntax, that keeps a Private
4701
+ * Creator Data Element whose value is longer than 65,534 bytes. Only a
4702
+ * `RetainSafePrivate` run whose `Profile` names a creator that long keeps one;
4703
+ * PS3.5 2026c section 6.2.2 forbids the `UN` such a value is otherwise written
4704
+ * as, so the Item has no encoding. The same element at the root is returned,
4705
+ * and `serializeDicom` refuses it with the same code.
4682
4706
  *
4683
4707
  * @example
4684
4708
  * ```ts
package/dist/index.d.ts CHANGED
@@ -2794,7 +2794,11 @@ declare class DicomValueError extends Error {
2794
2794
  * carries an offset that is not zero and names no Directory Record the
2795
2795
  * `Dataset` holds, or is not one 32-bit unsigned integer (never written stale
2796
2796
  * or as zero), or `DIRECTORY_OFFSET_DEFLATED` when a DICOMDIR under Deflated
2797
- * Explicit VR LE carries a non-zero offset. Nothing is returned on a throw.
2797
+ * Explicit VR LE carries a non-zero offset, or `VALUE_TOO_LONG_FOR_VR` when a
2798
+ * Private Creator Data Element under an Explicit VR syntax, or a File Meta
2799
+ * Information element under any, has a value past 65,534 bytes in a VR with a
2800
+ * 16-bit Value Length Field (PS3.5 2026c section 6.2.2 forbids the `UN` such a
2801
+ * value is otherwise written as). Nothing is returned on a throw.
2798
2802
  *
2799
2803
  * @example
2800
2804
  * ```ts
@@ -2849,6 +2853,13 @@ declare function serializeDicom(ds: Dataset): Buffer;
2849
2853
  * not one 32-bit unsigned integer). A position inside a deflated stream
2850
2854
  * names no Item a reader can seek to, so no offset can be written for it.
2851
2855
  * A Deflated DICOMDIR whose offsets are all zero is written.
2856
+ * - `VALUE_TOO_LONG_FOR_VR` - a Private Creator Data Element `(gggg,00xx)`
2857
+ * under an Explicit VR Transfer Syntax, or a File Meta Information element
2858
+ * `(0002,xxxx)` under any, carries a value longer than 65,534 bytes in a VR
2859
+ * whose Explicit VR Value Length Field is 16 bits. PS3.5 2026c section 6.2.2
2860
+ * has such a value written with VR `UN`, which the writer does for every
2861
+ * other element, but says the `UN` VR "shall not be used" for these two, so
2862
+ * the value has no encoding and nothing is written.
2852
2863
  *
2853
2864
  * The message is built only from structural constants (never a decoded
2854
2865
  * attribute value, a Transfer Syntax UID, or a length or byte read from the
@@ -2872,6 +2883,7 @@ declare const SERIALIZE_ERROR_CODES: {
2872
2883
  readonly INVALID_ENCAPSULATED_PIXEL_DATA: "INVALID_ENCAPSULATED_PIXEL_DATA";
2873
2884
  readonly DIRECTORY_OFFSET_UNRESOLVED: "DIRECTORY_OFFSET_UNRESOLVED";
2874
2885
  readonly DIRECTORY_OFFSET_DEFLATED: "DIRECTORY_OFFSET_DEFLATED";
2886
+ readonly VALUE_TOO_LONG_FOR_VR: "VALUE_TOO_LONG_FOR_VR";
2875
2887
  };
2876
2888
  /**
2877
2889
  * Discriminant for {@link DicomSerializeError.code}, enabling exhaustive
@@ -2892,6 +2904,8 @@ declare const SERIALIZE_ERROR_CODES: {
2892
2904
  * return "a DICOMDIR offset names no Directory Record the dataset holds";
2893
2905
  * case "DIRECTORY_OFFSET_DEFLATED":
2894
2906
  * return "a Deflated DICOMDIR carries a non-zero Directory Record offset";
2907
+ * case "VALUE_TOO_LONG_FOR_VR":
2908
+ * return "a Private Creator or File Meta value is too long for its VR";
2895
2909
  * }
2896
2910
  * }
2897
2911
  * ```
@@ -2920,6 +2934,9 @@ type SerializeErrorCode = (typeof SERIALIZE_ERROR_CODES)[keyof typeof SERIALIZE_
2920
2934
  * if (err instanceof DicomSerializeError && err.code === "DIRECTORY_OFFSET_UNRESOLVED") {
2921
2935
  * // a DICOMDIR offset the writer cannot tie to a Directory Record
2922
2936
  * }
2937
+ * if (err instanceof DicomSerializeError && err.code === "VALUE_TOO_LONG_FOR_VR") {
2938
+ * // a Private Creator or File Meta value past 65,534 bytes, which UN may not carry
2939
+ * }
2923
2940
  * }
2924
2941
  * ```
2925
2942
  */
@@ -4679,6 +4696,13 @@ declare class DeidentifyError extends Error {
4679
4696
  * @throws {@link DeidentifyError} (`INVALID_OPTIONS`) for an unknown Retain option,
4680
4697
  * for both PS3.15 §E.3.6 temporal Options in one call, or for a malformed
4681
4698
  * `uidRoot`.
4699
+ * @throws `DicomSerializeError` (`VALUE_TOO_LONG_FOR_VR`) when the run
4700
+ * re-encodes a Sequence Item, under an Explicit VR syntax, that keeps a Private
4701
+ * Creator Data Element whose value is longer than 65,534 bytes. Only a
4702
+ * `RetainSafePrivate` run whose `Profile` names a creator that long keeps one;
4703
+ * PS3.5 2026c section 6.2.2 forbids the `UN` such a value is otherwise written
4704
+ * as, so the Item has no encoding. The same element at the root is returned,
4705
+ * and `serializeDicom` refuses it with the same code.
4682
4706
  *
4683
4707
  * @example
4684
4708
  * ```ts
package/dist/index.mjs CHANGED
@@ -9,7 +9,7 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/version.ts
12
- var VERSION = "0.1.5";
12
+ var VERSION = "0.1.7";
13
13
 
14
14
  // src/dictionary/index.ts
15
15
  var dictionary_exports = {};
@@ -11139,8 +11139,24 @@ function directoryRecords(source) {
11139
11139
  }
11140
11140
  function csValues(el) {
11141
11141
  if (el === void 0) return void 0;
11142
- const text = el.rawBytes.toString("latin1").replace(/[\0 ]+$/u, "");
11143
- return Object.freeze(text === "" ? [] : text.split("\\").map((v) => v.replace(/^ +| +$/gu, "")));
11142
+ const text = stripTrailingPad(el.rawBytes.toString("latin1"));
11143
+ return Object.freeze(text === "" ? [] : text.split("\\").map(trimSpaces));
11144
+ }
11145
+ function stripTrailingPad(text) {
11146
+ let end = text.length;
11147
+ while (end > 0) {
11148
+ const code = text.charCodeAt(end - 1);
11149
+ if (code !== 0 && code !== 32) break;
11150
+ end--;
11151
+ }
11152
+ return text.slice(0, end);
11153
+ }
11154
+ function trimSpaces(value) {
11155
+ let start = 0;
11156
+ let end = value.length;
11157
+ while (start < end && value.charCodeAt(start) === 32) start++;
11158
+ while (end > start && value.charCodeAt(end - 1) === 32) end--;
11159
+ return value.slice(start, end);
11144
11160
  }
11145
11161
  function analyzeDirectory(source) {
11146
11162
  if (!isDicomdir(source.fileMeta)) return void 0;
@@ -13799,7 +13815,7 @@ function stripPad(bytes, tag, vr, position, warnings) {
13799
13815
  }
13800
13816
  return bytes.subarray(0, end);
13801
13817
  }
13802
- function trimSpaces(value) {
13818
+ function trimSpaces2(value) {
13803
13819
  let start = 0;
13804
13820
  let end = value.length;
13805
13821
  while (start < end && value.charCodeAt(start) === SPACE2) start++;
@@ -13858,7 +13874,7 @@ function decodeElementValue(element) {
13858
13874
  }
13859
13875
  if (vr === "LO" || vr === "SH" || vr === "UC") {
13860
13876
  const text = decodeCharsetString(trimmed, tag, vr, charset, position, warnings);
13861
- const values = text.split("\\").map(trimSpaces);
13877
+ const values = text.split("\\").map(trimSpaces2);
13862
13878
  return attachWarnings({ kind: "strings", values }, warnings);
13863
13879
  }
13864
13880
  if (vr === "LT" || vr === "ST" || vr === "UT") {
@@ -13871,7 +13887,7 @@ function decodeElementValue(element) {
13871
13887
  }
13872
13888
  if (vr === "AE" || vr === "CS" || vr === "AS" || vr === "UI") {
13873
13889
  const text = decodeAsciiString(trimmed, tag, vr, position, warnings);
13874
- const values = text.split("\\").map(trimSpaces);
13890
+ const values = text.split("\\").map(trimSpaces2);
13875
13891
  return attachWarnings({ kind: "strings", values }, warnings);
13876
13892
  }
13877
13893
  if (vr === "DS") {
@@ -14940,8 +14956,43 @@ function toDate(value, options) {
14940
14956
  const instant = new Date(utc.getTime() - offsetMinutes * 6e4);
14941
14957
  return Number.isNaN(instant.getTime()) ? void 0 : instant;
14942
14958
  }
14959
+
14960
+ // src/serialize/errors.ts
14961
+ var SERIALIZE_ERROR_CODES = {
14962
+ MISSING_TRANSFER_SYNTAX: "MISSING_TRANSFER_SYNTAX",
14963
+ UNSUPPORTED_TRANSFER_SYNTAX: "UNSUPPORTED_TRANSFER_SYNTAX",
14964
+ INVALID_ENCAPSULATED_PIXEL_DATA: "INVALID_ENCAPSULATED_PIXEL_DATA",
14965
+ DIRECTORY_OFFSET_UNRESOLVED: "DIRECTORY_OFFSET_UNRESOLVED",
14966
+ DIRECTORY_OFFSET_DEFLATED: "DIRECTORY_OFFSET_DEFLATED",
14967
+ VALUE_TOO_LONG_FOR_VR: "VALUE_TOO_LONG_FOR_VR"
14968
+ };
14969
+ var DicomSerializeError = class extends Error {
14970
+ code;
14971
+ /**
14972
+ * Construct a new `DicomSerializeError`. The `message` MUST be built only
14973
+ * from structural facts (never a decoded attribute value) so the error is
14974
+ * always safe to log.
14975
+ *
14976
+ * @internal
14977
+ */
14978
+ constructor(code, message) {
14979
+ super(`[${code}] ${message}`);
14980
+ this.name = "DicomSerializeError";
14981
+ this.code = code;
14982
+ }
14983
+ };
14984
+
14985
+ // src/serialize/element.ts
14943
14986
  var UNDEFINED_LENGTH4 = 4294967295;
14944
14987
  var MAX_SHORT_FORM_LENGTH = 65534;
14988
+ var PRIVATE_CREATOR_TOO_LONG_MESSAGE = "A Private Creator Data Element carries a value longer than 65,534 bytes under an Explicit VR Transfer Syntax, in a VR whose Value Length Field is 16 bits. PS3.5 section 6.2.2 has such a value written with VR UN, but says UN shall not be used for a Private Creator Data Element, so the value has no encoding and nothing is written.";
14989
+ var FILE_META_TOO_LONG_MESSAGE = "A File Meta Information element carries a value longer than 65,534 bytes in a VR whose Explicit VR Value Length Field is 16 bits. PS3.5 section 6.2.2 has such a value written with VR UN, but says UN shall not be used for a File Meta Information element, so the value has no encoding and nothing is written.";
14990
+ function refuseTooLongForVr(kind) {
14991
+ throw new DicomSerializeError(
14992
+ SERIALIZE_ERROR_CODES.VALUE_TOO_LONG_FOR_VR,
14993
+ kind === "privateCreator" ? PRIVATE_CREATOR_TOO_LONG_MESSAGE : FILE_META_TOO_LONG_MESSAGE
14994
+ );
14995
+ }
14945
14996
  var NULL_PAD_VRS = /* @__PURE__ */ new Set(["UI", "OB", "OW", "OF", "OD", "OL", "OV", "UN"]);
14946
14997
  function padValue(value, vr) {
14947
14998
  if (value.length % 2 === 0) return Buffer.from(value);
@@ -15004,7 +15055,9 @@ function encodeDatasetElement(el, encoding, rawBytes = el.rawBytes) {
15004
15055
  value
15005
15056
  ]);
15006
15057
  }
15007
- if (value.length > MAX_SHORT_FORM_LENGTH && !isPrivateCreator(group, element)) {
15058
+ if (value.length > MAX_SHORT_FORM_LENGTH) {
15059
+ if (isPrivateCreator(group, element)) refuseTooLongForVr("privateCreator");
15060
+ if (group === 2) refuseTooLongForVr("fileMeta");
15008
15061
  return Buffer.concat([
15009
15062
  uint16(group, littleEndian),
15010
15063
  uint16(element, littleEndian),
@@ -15023,30 +15076,6 @@ function encodeDatasetElement(el, encoding, rawBytes = el.rawBytes) {
15023
15076
  value
15024
15077
  ]);
15025
15078
  }
15026
-
15027
- // src/serialize/errors.ts
15028
- var SERIALIZE_ERROR_CODES = {
15029
- MISSING_TRANSFER_SYNTAX: "MISSING_TRANSFER_SYNTAX",
15030
- UNSUPPORTED_TRANSFER_SYNTAX: "UNSUPPORTED_TRANSFER_SYNTAX",
15031
- INVALID_ENCAPSULATED_PIXEL_DATA: "INVALID_ENCAPSULATED_PIXEL_DATA",
15032
- DIRECTORY_OFFSET_UNRESOLVED: "DIRECTORY_OFFSET_UNRESOLVED",
15033
- DIRECTORY_OFFSET_DEFLATED: "DIRECTORY_OFFSET_DEFLATED"
15034
- };
15035
- var DicomSerializeError = class extends Error {
15036
- code;
15037
- /**
15038
- * Construct a new `DicomSerializeError`. The `message` MUST be built only
15039
- * from structural facts (never a decoded attribute value) so the error is
15040
- * always safe to log.
15041
- *
15042
- * @internal
15043
- */
15044
- constructor(code, message) {
15045
- super(`[${code}] ${message}`);
15046
- this.name = "DicomSerializeError";
15047
- this.code = code;
15048
- }
15049
- };
15050
15079
  var UNDEFINED_LENGTH5 = 4294967295;
15051
15080
  var ITEM2 = 4294893568;
15052
15081
  var ITEM_DELIMITATION = 4294893581;
@@ -15491,6 +15520,7 @@ function encodeMetaElement(tag, vr, rawValue) {
15491
15520
  value
15492
15521
  ]);
15493
15522
  }
15523
+ if (value.length > MAX_SHORT_FORM_LENGTH) refuseTooLongForVr("fileMeta");
15494
15524
  const lengthBuf = Buffer.alloc(2);
15495
15525
  lengthBuf.writeUInt16LE(value.length, 0);
15496
15526
  return Buffer.concat([groupBuf, elementBuf, vrBuf, lengthBuf, value]);
@@ -16764,14 +16794,19 @@ function dummyBytes(vr) {
16764
16794
  const buf = Buffer.from(text, "latin1");
16765
16795
  return buf.length % 2 === 0 ? buf : Buffer.concat([buf, Buffer.from([32])]);
16766
16796
  }
16797
+ function stripTrailingNulls(text) {
16798
+ let end = text.length;
16799
+ while (end > 0 && text.charCodeAt(end - 1) === 0) end--;
16800
+ return text.slice(0, end);
16801
+ }
16767
16802
  function remapUidBytes(rawBytes, remap) {
16768
- const raw = rawBytes.toString("latin1").replace(/\0+$/, "");
16803
+ const raw = stripTrailingNulls(rawBytes.toString("latin1"));
16769
16804
  const remapped = raw.split("\\").map((uid2) => remap(uid2.trim())).join("\\");
16770
16805
  const buf = Buffer.from(remapped, "latin1");
16771
16806
  return buf.length % 2 === 0 ? buf : Buffer.concat([buf, Buffer.from([0])]);
16772
16807
  }
16773
16808
  function uidValueMultiplicity(rawBytes) {
16774
- const raw = rawBytes.toString("latin1").replace(/\0+$/, "");
16809
+ const raw = stripTrailingNulls(rawBytes.toString("latin1"));
16775
16810
  if (raw.length === 0) return 0;
16776
16811
  return raw.split("\\").length;
16777
16812
  }
@@ -17173,7 +17208,7 @@ function insertedSequence(tag, items, encoding) {
17173
17208
  });
17174
17209
  }
17175
17210
  function decodeCreator(el) {
17176
- return el.rawBytes.toString("latin1").replace(/[\0 ]+$/, "").trim();
17211
+ return trimTrailingPad(el.rawBytes).toString("latin1").trim();
17177
17212
  }
17178
17213
  function isPrivateCreatorElement(tag) {
17179
17214
  const { element } = splitTag(tag);
@@ -17301,7 +17336,7 @@ function declarationKey(group, creator) {
17301
17336
  return `${String(group)}:${creator}`;
17302
17337
  }
17303
17338
  function decodeEnumerated(el) {
17304
- return el.rawBytes.toString("latin1").replace(/[\0 ]+$/, "").trim();
17339
+ return trimTrailingPad(el.rawBytes).toString("latin1").trim();
17305
17340
  }
17306
17341
  function readUnsignedShorts(el) {
17307
17342
  const values = [];
@@ -17738,7 +17773,7 @@ function hasUncleanedBurnedIn(ds) {
17738
17773
  if (!ds.has(TAG_PIXEL_DATA)) return false;
17739
17774
  const flag = ds.get(TAG_BURNED_IN_ANNOTATION);
17740
17775
  if (flag === void 0) return true;
17741
- return flag.rawBytes.toString("latin1").replace(/[\0 ]+$/, "").trim().toUpperCase() !== "NO";
17776
+ return trimTrailingPad(flag.rawBytes).toString("latin1").trim().toUpperCase() !== "NO";
17742
17777
  }
17743
17778
  function deidentify(ds, options = {}) {
17744
17779
  if (isRefusedTransferSyntax(ds.fileMeta?.transferSyntaxUID)) {