@cosyte/dicom 0.0.8 โ†’ 0.0.11

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.mjs CHANGED
@@ -9,7 +9,7 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/version.ts
12
- var VERSION = "0.0.8";
12
+ var VERSION = "0.0.11";
13
13
 
14
14
  // src/dictionary/index.ts
15
15
  var dictionary_exports = {};
@@ -11701,15 +11701,24 @@ var BE_VR_STRIDE = Object.freeze({
11701
11701
  UT: 0,
11702
11702
  SQ: 0
11703
11703
  });
11704
+ var KNOWN_VRS = Object.freeze(
11705
+ new Set(Object.keys(BE_VR_STRIDE))
11706
+ );
11707
+ function isRecognizedVr(vr) {
11708
+ return KNOWN_VRS.has(vr);
11709
+ }
11704
11710
 
11705
11711
  // src/parser/warnings.ts
11706
11712
  var WARNING_CODES = {
11707
11713
  // === Phase 2 actively emits (D-08 active list - alphabetical-within-prefix per CONTEXT specifics ยง) ===
11714
+ DICOM_DUPLICATE_FILE_META_ELEMENT: "DICOM_DUPLICATE_FILE_META_ELEMENT",
11715
+ DICOM_DUPLICATE_TAG_IN_DATA_SET: "DICOM_DUPLICATE_TAG_IN_DATA_SET",
11708
11716
  DICOM_EMPTY_ITEM_IN_SEQUENCE: "DICOM_EMPTY_ITEM_IN_SEQUENCE",
11709
11717
  DICOM_FILE_META_GROUP_LENGTH_MISMATCH: "DICOM_FILE_META_GROUP_LENGTH_MISMATCH",
11710
11718
  DICOM_FILE_META_GROUP_LENGTH_MISSING: "DICOM_FILE_META_GROUP_LENGTH_MISSING",
11711
11719
  DICOM_GROUP_LENGTH_IN_DATASET: "DICOM_GROUP_LENGTH_IN_DATASET",
11712
11720
  DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR: "DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR",
11721
+ DICOM_ITEM_CROSSES_SEQUENCE_END: "DICOM_ITEM_CROSSES_SEQUENCE_END",
11713
11722
  DICOM_MISSING_PREAMBLE: "DICOM_MISSING_PREAMBLE",
11714
11723
  DICOM_NONZERO_RESERVED_BYTES: "DICOM_NONZERO_RESERVED_BYTES",
11715
11724
  DICOM_ODD_LENGTH_VALUE_PADDED: "DICOM_ODD_LENGTH_VALUE_PADDED",
@@ -11735,6 +11744,10 @@ var WARNING_CODES = {
11735
11744
  // reserved by Phase 7 - not emitted in Phase 2
11736
11745
  DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED: "DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED",
11737
11746
  // emitted by deidentify(), never by the parser
11747
+ DICOM_DEIDENT_METHOD_NOT_ADDED: "DICOM_DEIDENT_METHOD_NOT_ADDED",
11748
+ // emitted by deidentify(), never by the parser
11749
+ DICOM_DEIDENT_METHOD_PRIOR_RETAINED: "DICOM_DEIDENT_METHOD_PRIOR_RETAINED",
11750
+ // emitted by deidentify(), never by the parser
11738
11751
  DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE: "DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE",
11739
11752
  // emitted by deidentify(), never by the parser
11740
11753
  DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE: "DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE",
@@ -11751,10 +11764,44 @@ var WARNING_MESSAGES = Object.freeze({
11751
11764
  DICOM_VR_MISMATCH: "Element ({tag}) on-wire VR is {vr2}; dictionary lists {vr}. Trusting on-wire VR.",
11752
11765
  DICOM_PRIVATE_TAG_NO_CREATOR: "Private element ({tag}) has no Private Creator registered for its block; treating as VR=UN.",
11753
11766
  DICOM_GROUP_LENGTH_IN_DATASET: "Retired Group Length element ({tag}) encountered in dataset; preserved as-is.",
11754
- DICOM_NONZERO_RESERVED_BYTES: "Element ({tag}) has non-zero reserved bytes between VR and length (first byte {n}, second byte {n2}); ignoring.",
11767
+ // The tag is deliberately absent, and this is the only *parser* message where
11768
+ // that is true. See `nonzeroReservedBytes`. Kept terse for the same reason the
11769
+ // de-identify messages are: it is emitted once per element, and element count
11770
+ // is attacker-chosen.
11771
+ DICOM_NONZERO_RESERVED_BYTES: "Non-zero reserved bytes between VR and length (first byte {n}, second byte {n2}); ignoring. Tag withheld; the byte offset identifies the element.",
11755
11772
  DICOM_SQ_NOT_DESCENDED: "Element ({tag}) resolved to VR=SQ from the dictionary but its defined-length value is not a valid item stream; kept as opaque bytes and NOT descended, so nothing nested inside it is visible to navigation, and deidentify() cannot audit it. See DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE.",
11756
11773
  DICOM_UN_PARSED_AS_SQ: "Element ({tag}) has VR=UN with undefined length; descended as Implicit VR LE sequence per CP-246.",
11757
11774
  DICOM_EMPTY_ITEM_IN_SEQUENCE: "Sequence ({tag}) contains an empty item (length=0); tolerated.",
11775
+ // Neither the tag nor the replaced element's value is here, and the tag's
11776
+ // absence is specific to this code rather than caution. See
11777
+ // `duplicateTagInDataSet`. Short by measurement rather than by intention: one
11778
+ // is raised per collision and the collision count is chosen by the input, so a
11779
+ // 400-character draft was measured at 50 M characters over a 1 MiB file and
11780
+ // cut. The reasoning lives in the factory's JSDoc, not in the string.
11781
+ DICOM_DUPLICATE_TAG_IN_DATA_SET: "A Data Element carrying a tag this Data Set already holds replaced the earlier one at this byte offset; that element's value is not in the parsed object. PS3.5 7.1 and 7.5.1. Tag withheld.",
11782
+ // The tag is deliberately absent for the same reason as the code above, and
11783
+ // the surviving copy is named because the File Meta group resolves a repeat
11784
+ // the OPPOSITE way round to a Data Set: first copy wins here, last read wins
11785
+ // there. Short for the same measured reason - one is raised per repeat and the
11786
+ // repeat count is chosen by the input.
11787
+ // ๐Ÿ›‘ NO SPEC CITATION HERE, AND ITS ABSENCE IS DELIBERATE. A draft ended this
11788
+ // string "PS3.5 7.1.", which asserts that section's "at most once in a Data
11789
+ // Set" governs the File Meta group - the applicability this slice states it
11790
+ // cannot establish, because PS3.10 governs that group and is not vendored
11791
+ // here. Under `{ strict: true }` this string is also the thrown message, so an
11792
+ // unqualified citation there is the claim, not a footnote. Do not put it back.
11793
+ DICOM_DUPLICATE_FILE_META_ELEMENT: "A File Meta Data Element repeats a (0002,xxxx) tag the group already carries; the copy at this byte offset is not in the parsed File Meta, and the FIRST copy of that tag is the one projected. Tag withheld.",
11794
+ // No value and no length: the prior value is the file's own text. The tag is a
11795
+ // constant of this code rather than composed from input, as in the two
11796
+ // (0002,0000) messages above.
11797
+ DICOM_DEIDENT_METHOD_NOT_ADDED: "The De-identification Method this run recorded could not be added beside the value (0012,0063) already carried without exceeding the largest Value Length that VR can encode, so the earlier value was replaced (PS3.15 E.1.1). The replaced text is not in the output.",
11798
+ // No value and no length, for the same reason as the code above: the retained
11799
+ // text is the file's own. The tag is a constant of this code.
11800
+ DICOM_DEIDENT_METHOD_PRIOR_RETAINED: "A De-identification Method (0012,0063) value the source file already carried was kept beside the one this run recorded, as PS3.15 E.1.1 requires. That attribute is not in Table E.1-1, so no rule in this run inspected, audited or redacted those bytes: if the sender wrote identifying text there it is in the de-identified output, under (0012,0062) = YES. The text is withheld from this message.",
11801
+ // The Item's own declared length is deliberately absent. See
11802
+ // `itemCrossesSequenceEnd`; `{n2}` stays because the emit site's
11803
+ // `endLimit < buffer.length` conjunct bounds it by the buffer.
11804
+ DICOM_ITEM_CROSSES_SEQUENCE_END: "Item ({tag}) declares a length reaching past its enclosing sequence's declared end, so it reads the enclosing Data Set's bytes; {n2} bytes remained inside the sequence. The file's two length fields disagree (PS3.5 7.5.1 and 7.5.2 govern them); the item's is used. The declared length is withheld; the byte offset locates the item.",
11758
11805
  DICOM_PIXEL_DATA_LENGTH_MISMATCH: "(7FE0,0010) PixelData declared length {n} does not match computed {n2} bytes.",
11759
11806
  DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR: "Private element ({tag}) under Implicit VR LE has no VR override; falling back to UN.",
11760
11807
  DICOM_PRIVATE_CREATOR_UNKNOWN: "Private element ({tag}) has a Private Creator the active profile's private dictionary does not name; falling back to UN. The creator string is not reproduced here - read the (gggg,00EE) element if you need it.",
@@ -11786,7 +11833,6 @@ var WARNING_MESSAGES = Object.freeze({
11786
11833
  DICOM_UNSUPPORTED_CHARSET: "(0008,0005) Specific Character Set value {n} names a defined term this build does not support; decoding text as UTF-8 best-effort. The term is not reproduced here - read the element if you need it.",
11787
11834
  DICOM_CHARSET_AMBIGUOUS_SEPARATOR: "(0008,0005) Specific Character Set value {n} is ambiguous under the active code extensions; decoding text as UTF-8 best-effort."
11788
11835
  });
11789
- var KNOWN_VRS = new Set(Object.keys(BE_VR_STRIDE));
11790
11836
  var TAG_SHAPE = /^[0-9A-F]{8}$/u;
11791
11837
  function renderTag(tag) {
11792
11838
  return tag !== void 0 && TAG_SHAPE.test(tag) ? tag : WITHHELD;
@@ -11828,9 +11874,8 @@ function privateTagNoCreator(position, tag) {
11828
11874
  function groupLengthInDataset(position, tag) {
11829
11875
  return build(WARNING_CODES.DICOM_GROUP_LENGTH_IN_DATASET, position, { tag });
11830
11876
  }
11831
- function nonzeroReservedBytes(position, tag, first, second) {
11877
+ function nonzeroReservedBytes(position, first, second) {
11832
11878
  return build(WARNING_CODES.DICOM_NONZERO_RESERVED_BYTES, position, {
11833
- tag,
11834
11879
  n: first,
11835
11880
  n2: second
11836
11881
  });
@@ -11841,9 +11886,21 @@ function unParsedAsSQ(position, tag) {
11841
11886
  function sqNotDescended(position, tag) {
11842
11887
  return build(WARNING_CODES.DICOM_SQ_NOT_DESCENDED, position, { tag });
11843
11888
  }
11889
+ function itemCrossesSequenceEnd(position, tag, availableLength) {
11890
+ return build(WARNING_CODES.DICOM_ITEM_CROSSES_SEQUENCE_END, position, {
11891
+ tag,
11892
+ n2: availableLength
11893
+ });
11894
+ }
11844
11895
  function emptyItemInSequence(position, tag) {
11845
11896
  return build(WARNING_CODES.DICOM_EMPTY_ITEM_IN_SEQUENCE, position, { tag });
11846
11897
  }
11898
+ function duplicateTagInDataSet(position) {
11899
+ return build(WARNING_CODES.DICOM_DUPLICATE_TAG_IN_DATA_SET, position);
11900
+ }
11901
+ function duplicateFileMetaElement(position) {
11902
+ return build(WARNING_CODES.DICOM_DUPLICATE_FILE_META_ELEMENT, position);
11903
+ }
11847
11904
  function implicitVRForPrivateTagWithoutVR(position, tag) {
11848
11905
  return build(WARNING_CODES.DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR, position, { tag });
11849
11906
  }
@@ -11853,6 +11910,12 @@ function privateCreatorUnknown(position, tag) {
11853
11910
  function burnedInAnnotationNotRemoved(position) {
11854
11911
  return build(WARNING_CODES.DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED, position);
11855
11912
  }
11913
+ function deidentMethodNotAdded(position) {
11914
+ return build(WARNING_CODES.DICOM_DEIDENT_METHOD_NOT_ADDED, position);
11915
+ }
11916
+ function deidentMethodPriorRetained(position) {
11917
+ return build(WARNING_CODES.DICOM_DEIDENT_METHOD_PRIOR_RETAINED, position);
11918
+ }
11856
11919
  function embeddedAttributeRemoved(position, tag, vr, count) {
11857
11920
  return build(WARNING_CODES.DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED, position, {
11858
11921
  tag,
@@ -12034,12 +12097,12 @@ function readExplicitElementHeader(cursor, _ctx, emit) {
12034
12097
  const vr = vrSlice.toString("ascii");
12035
12098
  let length;
12036
12099
  let headerLength;
12037
- if (LONG_FORM_VRS.has(vr)) {
12100
+ if (LONG_FORM_VRS.has(vr) || !isRecognizedVr(vr)) {
12038
12101
  const reserved0 = cursor.buffer[cursor.position];
12039
12102
  const reserved1 = cursor.buffer[cursor.position + 1];
12040
12103
  cursor.position += 2;
12041
12104
  if ((reserved0 ?? 0) !== 0 || (reserved1 ?? 0) !== 0) {
12042
- emit(nonzeroReservedBytes({ byteOffset: headerStart }, tag, reserved0 ?? 0, reserved1 ?? 0));
12105
+ emit(nonzeroReservedBytes({ byteOffset: headerStart }, reserved0 ?? 0, reserved1 ?? 0));
12043
12106
  }
12044
12107
  length = cursor.readUInt32();
12045
12108
  headerLength = 12;
@@ -12147,6 +12210,16 @@ function parseFileMeta(buffer, datasetStart, ctx, emit) {
12147
12210
  fmElements.push(next);
12148
12211
  }
12149
12212
  }
12213
+ const seenModeledTags = /* @__PURE__ */ new Set();
12214
+ if (declaredFmLength !== void 0) seenModeledTags.add("00020000");
12215
+ for (const e of fmElements) {
12216
+ if (!MODELED_FM_TAGS.has(e.tag)) continue;
12217
+ if (seenModeledTags.has(e.tag)) {
12218
+ emit(duplicateFileMetaElement({ byteOffset: e.byteOffset, fileMeta: true }));
12219
+ continue;
12220
+ }
12221
+ seenModeledTags.add(e.tag);
12222
+ }
12150
12223
  const tsElement = fmElements.find((e) => e.tag === "00020010");
12151
12224
  if (tsElement === void 0 || tsElement.vr !== "UI") {
12152
12225
  throw new DicomParseError(
@@ -12177,7 +12250,7 @@ function readExplicitLeElement(cursor) {
12177
12250
  const vr = cursor.slice(2).toString("ascii");
12178
12251
  let length;
12179
12252
  let headerLength;
12180
- if (LONG_FORM_VRS.has(vr)) {
12253
+ if (LONG_FORM_VRS.has(vr) || !isRecognizedVr(vr)) {
12181
12254
  cursor.slice(2);
12182
12255
  length = cursor.readUInt32();
12183
12256
  headerLength = 12;
@@ -12193,7 +12266,7 @@ function readExplicitLeElement(cursor) {
12193
12266
  if (bytesConsumed !== headerLength + length) {
12194
12267
  throw new RangeError("File Meta element accounting mismatch");
12195
12268
  }
12196
- return { tag, vr, value, bytesConsumed };
12269
+ return { tag, vr, value, bytesConsumed, byteOffset: headerStart };
12197
12270
  }
12198
12271
  function trimUI(buf) {
12199
12272
  let end = buf.length;
@@ -12633,6 +12706,14 @@ var Element = class {
12633
12706
  }
12634
12707
  };
12635
12708
 
12709
+ // src/parser/data-set-map.ts
12710
+ function defineElement(elements, element, emit) {
12711
+ if (elements.has(element.tag)) {
12712
+ emit(duplicateTagInDataSet({ byteOffset: element.byteOffset }));
12713
+ }
12714
+ elements.set(element.tag, element);
12715
+ }
12716
+
12636
12717
  // src/dataset/item.ts
12637
12718
  var Item = class extends Dataset {
12638
12719
  index;
@@ -12754,6 +12835,15 @@ function parseSequence(buffer, valueStart, ctx, emit, opts) {
12754
12835
  items.push(new Item({ warnings: [], elements: inner.elements, index: itemIndex }));
12755
12836
  cursor.position = inner.endOffset;
12756
12837
  } else {
12838
+ if (opts.explicitLength !== void 0 && endLimit < buffer.length && cursor.position + itemLength > endLimit) {
12839
+ emit(
12840
+ itemCrossesSequenceEnd(
12841
+ { byteOffset: itemHeaderStart },
12842
+ itemTag,
12843
+ Math.max(0, endLimit - cursor.position)
12844
+ )
12845
+ );
12846
+ }
12757
12847
  if (cursor.position + itemLength > buffer.length) {
12758
12848
  throw new DicomParseError(
12759
12849
  FATAL_CODES.INVALID_FILE_META,
@@ -12889,8 +12979,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12889
12979
  cursor.position = seq.endOffset;
12890
12980
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(valueRawStart, cursor.position)) : buffer.subarray(valueRawStart, cursor.position);
12891
12981
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
12892
- elements.set(
12893
- tag,
12982
+ defineElement(
12983
+ elements,
12894
12984
  new Element({
12895
12985
  tag,
12896
12986
  vr,
@@ -12901,7 +12991,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12901
12991
  littleEndian: true,
12902
12992
  items: seq.items,
12903
12993
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
12904
- })
12994
+ }),
12995
+ emit
12905
12996
  );
12906
12997
  continue;
12907
12998
  }
@@ -12920,8 +13011,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12920
13011
  cursor.position = cp246.endOffset;
12921
13012
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(valueRawStart, cursor.position)) : buffer.subarray(valueRawStart, cursor.position);
12922
13013
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
12923
- elements.set(
12924
- tag,
13014
+ defineElement(
13015
+ elements,
12925
13016
  new Element({
12926
13017
  tag,
12927
13018
  vr: "SQ",
@@ -12934,7 +13025,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12934
13025
  littleEndian: true,
12935
13026
  items: cp246.items,
12936
13027
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
12937
- })
13028
+ }),
13029
+ emit
12938
13030
  );
12939
13031
  continue;
12940
13032
  }
@@ -12970,8 +13062,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12970
13062
  );
12971
13063
  if (seq.success) {
12972
13064
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
12973
- elements.set(
12974
- tag,
13065
+ defineElement(
13066
+ elements,
12975
13067
  new Element({
12976
13068
  tag,
12977
13069
  vr,
@@ -12982,7 +13074,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12982
13074
  littleEndian: true,
12983
13075
  items: seq.items,
12984
13076
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
12985
- })
13077
+ }),
13078
+ emit
12986
13079
  );
12987
13080
  continue;
12988
13081
  }
@@ -12995,8 +13088,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12995
13088
  }
12996
13089
  applySpecificCharacterSet(tag, valueSlice, ctx, emit, position);
12997
13090
  const privateCreator = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
12998
- elements.set(
12999
- tag,
13091
+ defineElement(
13092
+ elements,
13000
13093
  new Element({
13001
13094
  tag,
13002
13095
  vr,
@@ -13008,7 +13101,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
13008
13101
  littleEndian: true,
13009
13102
  ...ctx.currentCharset !== void 0 ? { specificCharacterSet: ctx.currentCharset } : {},
13010
13103
  ...privateCreator !== void 0 ? { privateCreator } : {}
13011
- })
13104
+ }),
13105
+ emit
13012
13106
  );
13013
13107
  }
13014
13108
  return { elements, endOffset: cursor.position };
@@ -13115,8 +13209,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13115
13209
  cursor.position = seqResult.endOffset;
13116
13210
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, cursor.position)) : buffer.subarray(headerStart, cursor.position);
13117
13211
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13118
- elements.set(
13119
- tag,
13212
+ defineElement(
13213
+ elements,
13120
13214
  new Element({
13121
13215
  tag,
13122
13216
  vr: "SQ",
@@ -13127,7 +13221,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13127
13221
  littleEndian: mode.littleEndian,
13128
13222
  items: seqResult.items,
13129
13223
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
13130
- })
13224
+ }),
13225
+ emit
13131
13226
  );
13132
13227
  continue;
13133
13228
  }
@@ -13142,8 +13237,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13142
13237
  const result = parseSequence(buffer, valueStart2, ctx, emit, seqOpts);
13143
13238
  cursor.position = result.endOffset;
13144
13239
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, cursor.position)) : buffer.subarray(headerStart, cursor.position);
13145
- elements.set(
13146
- tag,
13240
+ defineElement(
13241
+ elements,
13147
13242
  new Element({
13148
13243
  tag,
13149
13244
  vr: "OB",
@@ -13154,7 +13249,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13154
13249
  byteOffset: headerStart,
13155
13250
  littleEndian: mode.littleEndian,
13156
13251
  items: result.items
13157
- })
13252
+ }),
13253
+ emit
13158
13254
  );
13159
13255
  continue;
13160
13256
  }
@@ -13173,8 +13269,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13173
13269
  cursor.position = cp246.endOffset;
13174
13270
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, cursor.position)) : buffer.subarray(headerStart, cursor.position);
13175
13271
  const privateCreator3 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13176
- elements.set(
13177
- tag,
13272
+ defineElement(
13273
+ elements,
13178
13274
  new Element({
13179
13275
  tag,
13180
13276
  vr: "SQ",
@@ -13187,15 +13283,16 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13187
13283
  littleEndian: mode.littleEndian,
13188
13284
  items: cp246.items,
13189
13285
  ...privateCreator3 !== void 0 ? { privateCreator: privateCreator3 } : {}
13190
- })
13286
+ }),
13287
+ emit
13191
13288
  );
13192
13289
  continue;
13193
13290
  }
13194
13291
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13195
13292
  const fallbackEnd = buffer.length;
13196
13293
  const fallbackBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, fallbackEnd)) : buffer.subarray(headerStart, fallbackEnd);
13197
- elements.set(
13198
- tag,
13294
+ defineElement(
13295
+ elements,
13199
13296
  new Element({
13200
13297
  tag,
13201
13298
  vr: "UN",
@@ -13205,7 +13302,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13205
13302
  byteOffset: headerStart,
13206
13303
  littleEndian: mode.littleEndian,
13207
13304
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
13208
- })
13305
+ }),
13306
+ emit
13209
13307
  );
13210
13308
  cursor.position = fallbackEnd;
13211
13309
  continue;
@@ -13240,8 +13338,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13240
13338
  applySpecificCharacterSet(tag, valueSlice, ctx, emit, position);
13241
13339
  const privateCreator = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13242
13340
  const finalVr = vr;
13243
- elements.set(
13244
- tag,
13341
+ defineElement(
13342
+ elements,
13245
13343
  new Element({
13246
13344
  tag,
13247
13345
  vr: finalVr,
@@ -13253,7 +13351,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13253
13351
  littleEndian: mode.littleEndian,
13254
13352
  ...ctx.currentCharset !== void 0 ? { specificCharacterSet: ctx.currentCharset } : {},
13255
13353
  ...privateCreator !== void 0 ? { privateCreator } : {}
13256
- })
13354
+ }),
13355
+ emit
13257
13356
  );
13258
13357
  }
13259
13358
  return { elements, endOffset: cursor.position };
@@ -13451,7 +13550,7 @@ function encodeDatasetElement(el, encoding) {
13451
13550
  }
13452
13551
  const littleEndian = encoding === "explicitLE";
13453
13552
  const vrBuf = Buffer.from(el.vr, "ascii");
13454
- if (LONG_FORM_VRS.has(el.vr)) {
13553
+ if (LONG_FORM_VRS.has(el.vr) || !isRecognizedVr(el.vr)) {
13455
13554
  return Buffer.concat([
13456
13555
  uint16(group, littleEndian),
13457
13556
  uint16(element, littleEndian),
@@ -13502,7 +13601,7 @@ function encodeMetaElement(tag, vr, rawValue) {
13502
13601
  const elementBuf = Buffer.alloc(2);
13503
13602
  elementBuf.writeUInt16LE(element, 0);
13504
13603
  const vrBuf = Buffer.from(vr, "ascii");
13505
- if (LONG_FORM_VRS.has(vr)) {
13604
+ if (LONG_FORM_VRS.has(vr) || !isRecognizedVr(vr)) {
13506
13605
  const lengthBuf2 = Buffer.alloc(4);
13507
13606
  lengthBuf2.writeUInt32LE(value.length, 0);
13508
13607
  return Buffer.concat([
@@ -14697,7 +14796,6 @@ function uidValueMultiplicity(rawBytes) {
14697
14796
  }
14698
14797
 
14699
14798
  // src/deident/embedded.ts
14700
- var KNOWN_VRS2 = new Set(Object.keys(BE_VR_STRIDE));
14701
14799
  var UNDEFINED_LENGTH4 = 4294967295;
14702
14800
  var FFFE_GROUP = 65534;
14703
14801
  var SCANNABLE_CARRIER_VRS = /* @__PURE__ */ new Set([
@@ -14758,7 +14856,7 @@ function decodeAt(value, off, encoding) {
14758
14856
  return next2 > value.length ? void 0 : { next: next2, tag };
14759
14857
  }
14760
14858
  const vr = value.toString("latin1", off + 4, off + 6);
14761
- if (!KNOWN_VRS2.has(vr)) return void 0;
14859
+ if (!KNOWN_VRS.has(vr)) return void 0;
14762
14860
  if (LONG_FORM_VRS.has(vr)) {
14763
14861
  if (off + 12 > value.length) return void 0;
14764
14862
  if (value[off + 6] !== 0 || value[off + 7] !== 0) return void 0;
@@ -14895,7 +14993,6 @@ var BODY_ENCODING2 = {
14895
14993
  };
14896
14994
  var MAX_UNAUDITABLE_SEQUENCE_FINDINGS = 64;
14897
14995
  var MAX_UNDEFINED_VR_FINDINGS = 64;
14898
- var KNOWN_VRS3 = new Set(Object.keys(BE_VR_STRIDE));
14899
14996
  function validateRetain(retain) {
14900
14997
  const valid = new Set(DEIDENTIFY_OPTIONS);
14901
14998
  const active = /* @__PURE__ */ new Set();
@@ -15016,7 +15113,7 @@ function actsOnTag(tag, ctx) {
15016
15113
  return resolveAction(effectiveCode(action, ctx.active)) !== "K";
15017
15114
  }
15018
15115
  function hasUndefinedVr(el) {
15019
- return !KNOWN_VRS3.has(el.vr);
15116
+ return !KNOWN_VRS.has(el.vr);
15020
15117
  }
15021
15118
  function keepOrEmpty(el, ctx, contextPath, out) {
15022
15119
  if (hasUndefinedVr(el)) {
@@ -15080,6 +15177,26 @@ function creatorFor(tag, creators) {
15080
15177
  const { group, element } = splitTag(tag);
15081
15178
  return creators.get(`${String(group)}:${String(element >> 8 & 255)}`);
15082
15179
  }
15180
+ var UNDEFINED_LENGTH5 = 4294967295;
15181
+ var EXPLICIT_SQ_HEADER_BYTES = 12;
15182
+ function itemStreamOverrunsSequence(el, encoding) {
15183
+ if (el.vr !== "SQ" || el.length === UNDEFINED_LENGTH5) return false;
15184
+ const headerBytes = encoding === "implicit" ? 0 : EXPLICIT_SQ_HEADER_BYTES;
15185
+ return el.rawBytes.length - headerBytes > el.length;
15186
+ }
15187
+ function settledBound(source, encoding) {
15188
+ let index = source.length;
15189
+ let byteOffset;
15190
+ for (const [at, el] of source.entries()) {
15191
+ if (!itemStreamOverrunsSequence(el, encoding) && !isUnauditableSequence(el)) continue;
15192
+ if (index === source.length) index = at + 1;
15193
+ if (byteOffset === void 0 || el.byteOffset < byteOffset) byteOffset = el.byteOffset;
15194
+ }
15195
+ return { index, byteOffset };
15196
+ }
15197
+ function isSettled(el, at, bound) {
15198
+ return at < bound.index && (bound.byteOffset === void 0 || el.byteOffset <= bound.byteOffset);
15199
+ }
15083
15200
  function keepsPrivate(el, ctx, creators) {
15084
15201
  if (!ctx.active.has("RetainSafePrivate") || ctx.profile === void 0) return false;
15085
15202
  if (isPrivateCreatorElement(el.tag)) {
@@ -15089,7 +15206,7 @@ function keepsPrivate(el, ctx, creators) {
15089
15206
  if (creator === void 0) return false;
15090
15207
  return resolvePrivateTag(ctx.profile, el.tag, creator) !== void 0;
15091
15208
  }
15092
- function processElements(source, ctx, contextPath) {
15209
+ function processElements(source, ctx, contextPath, reservationsUsable) {
15093
15210
  const out = {
15094
15211
  elements: /* @__PURE__ */ new Map(),
15095
15212
  attributes: [],
@@ -15099,10 +15216,12 @@ function processElements(source, ctx, contextPath) {
15099
15216
  undefinedVrElements: [],
15100
15217
  warnings: []
15101
15218
  };
15102
- const creators = creatorsInScope(source);
15103
- for (const el of source) {
15219
+ const bound = settledBound(source, ctx.encoding);
15220
+ const creators = creatorsInScope(source.filter((el, at) => isSettled(el, at, bound)));
15221
+ for (const [at, el] of source.entries()) {
15104
15222
  if (isPrivateTag(el.tag)) {
15105
- if (keepsPrivate(el, ctx, creators)) keepOrEmpty(el, ctx, contextPath, out);
15223
+ if (reservationsUsable && isSettled(el, at, bound) && keepsPrivate(el, ctx, creators))
15224
+ keepOrEmpty(el, ctx, contextPath, out);
15106
15225
  else out.removedPrivateTags.push(el.tag);
15107
15226
  continue;
15108
15227
  }
@@ -15110,7 +15229,8 @@ function processElements(source, ctx, contextPath) {
15110
15229
  if (action === void 0) {
15111
15230
  if (el.vr === "SQ") {
15112
15231
  if (isUnauditableSequence(el)) emptyUnauditableSequence(el, ctx, contextPath, out);
15113
- else out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out));
15232
+ else
15233
+ out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out, reservationsUsable));
15114
15234
  } else {
15115
15235
  keepOrEmpty(el, ctx, contextPath, out);
15116
15236
  }
@@ -15118,7 +15238,7 @@ function processElements(source, ctx, contextPath) {
15118
15238
  }
15119
15239
  const resolved = resolveAction(effectiveCode(action, ctx.active));
15120
15240
  if (el.vr === "SQ") {
15121
- applySequenceAction(el, resolved, action, ctx, contextPath, out);
15241
+ applySequenceAction(el, resolved, action, ctx, contextPath, out, reservationsUsable);
15122
15242
  continue;
15123
15243
  }
15124
15244
  let applied;
@@ -15159,11 +15279,12 @@ function processElements(source, ctx, contextPath) {
15159
15279
  }
15160
15280
  return out;
15161
15281
  }
15162
- function descendSequence(el, ctx, contextPath, out) {
15282
+ function descendSequence(el, ctx, contextPath, out, reservationsUsable) {
15283
+ const childReservationsUsable = reservationsUsable && !itemStreamOverrunsSequence(el, ctx.encoding);
15163
15284
  const newItems = [];
15164
15285
  (el.items ?? []).forEach((item, index) => {
15165
15286
  const childPath = [...contextPath, `${el.tag}[${String(index)}]`];
15166
- const inner = processElements(item.elements(), ctx, childPath);
15287
+ const inner = processElements(item.elements(), ctx, childPath, childReservationsUsable);
15167
15288
  out.attributes.push(...inner.attributes);
15168
15289
  out.removedPrivateTags.push(...inner.removedPrivateTags);
15169
15290
  out.embeddedAttributes.push(...inner.embeddedAttributes);
@@ -15174,7 +15295,7 @@ function descendSequence(el, ctx, contextPath, out) {
15174
15295
  });
15175
15296
  return rebuildSequence(el, newItems, ctx.encoding);
15176
15297
  }
15177
- function applySequenceAction(el, resolved, action, ctx, contextPath, out) {
15298
+ function applySequenceAction(el, resolved, action, ctx, contextPath, out, reservationsUsable) {
15178
15299
  let applied;
15179
15300
  switch (resolved) {
15180
15301
  case "X":
@@ -15190,7 +15311,7 @@ function applySequenceAction(el, resolved, action, ctx, contextPath, out) {
15190
15311
  emptyUnauditableSequence(el, ctx, contextPath, out);
15191
15312
  applied = "emptied";
15192
15313
  } else {
15193
- out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out));
15314
+ out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out, reservationsUsable));
15194
15315
  applied = "cleaned";
15195
15316
  }
15196
15317
  break;
@@ -15200,7 +15321,7 @@ function applySequenceAction(el, resolved, action, ctx, contextPath, out) {
15200
15321
  emptyUnauditableSequence(el, ctx, contextPath, out);
15201
15322
  applied = "emptied";
15202
15323
  } else {
15203
- out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out));
15324
+ out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out, reservationsUsable));
15204
15325
  applied = "kept";
15205
15326
  }
15206
15327
  break;
@@ -15232,6 +15353,47 @@ function defaultMethod(active) {
15232
15353
  const base = "Cosyte @cosyte/dicom: PS3.15 Basic Application Level Confidentiality Profile";
15233
15354
  return active.size === 0 ? base : `${base} + ${[...active].join(", ")}`;
15234
15355
  }
15356
+ var VALUE_DELIMITER = 92;
15357
+ var MAX_SHORT_FORM_VALUE_BYTES = 65534;
15358
+ function addDeidentificationMethod(existing, method) {
15359
+ const added = trimTrailingPad(Buffer.from(method, "latin1"));
15360
+ if (existing === void 0 || existing.vr !== "LO") {
15361
+ return { value: added, replacedPrior: false, retainedPrior: false };
15362
+ }
15363
+ const kept = Buffer.from(trimTrailingPad(existing.rawBytes));
15364
+ if (kept.length === 0) return { value: added, replacedPrior: false, retainedPrior: false };
15365
+ const keptValues = splitValues(kept).map(trimTrailingPad);
15366
+ const missing = added.length === 0 ? [] : splitValues(added).filter((v) => !keptValues.some((k) => k.equals(trimTrailingPad(v))));
15367
+ const parts = [kept];
15368
+ for (const value2 of missing) {
15369
+ parts.push(Buffer.from([VALUE_DELIMITER]), value2);
15370
+ }
15371
+ const value = trimTrailingPad(Buffer.concat(parts));
15372
+ if (value.length > MAX_SHORT_FORM_VALUE_BYTES) {
15373
+ return { value: added, replacedPrior: true, retainedPrior: false };
15374
+ }
15375
+ return { value, replacedPrior: false, retainedPrior: true };
15376
+ }
15377
+ function trimTrailingPad(value) {
15378
+ let end = value.length;
15379
+ while (end > 0) {
15380
+ const last = value[end - 1];
15381
+ if (last === 32 || last === 0) end--;
15382
+ else break;
15383
+ }
15384
+ return value.subarray(0, end);
15385
+ }
15386
+ function splitValues(value) {
15387
+ const out = [];
15388
+ let start = 0;
15389
+ for (let i = 0; i <= value.length; i++) {
15390
+ if (i === value.length || value[i] === VALUE_DELIMITER) {
15391
+ out.push(value.subarray(start, i));
15392
+ start = i + 1;
15393
+ }
15394
+ }
15395
+ return out;
15396
+ }
15235
15397
  function hasUncleanedBurnedIn(ds) {
15236
15398
  if (!ds.has(TAG_PIXEL_DATA)) return false;
15237
15399
  const flag = ds.get(TAG_BURNED_IN_ANNOTATION);
@@ -15251,7 +15413,7 @@ function deidentify(ds, options = {}) {
15251
15413
  encoding,
15252
15414
  budget: { unauditableSequences: 0, undefinedVrElements: 0 }
15253
15415
  };
15254
- const processed = processElements(ds.elements(), ctx, []);
15416
+ const processed = processElements(ds.elements(), ctx, [], true);
15255
15417
  const {
15256
15418
  elements,
15257
15419
  attributes,
@@ -15265,11 +15427,23 @@ function deidentify(ds, options = {}) {
15265
15427
  insertedScalar(TAG_PATIENT_IDENTITY_REMOVED, "CS", Buffer.from("YES", "latin1"), littleEndian)
15266
15428
  );
15267
15429
  const method = options.deidentificationMethod ?? defaultMethod(active);
15430
+ const priorMethod = elements.get(TAG_DEIDENTIFICATION_METHOD);
15431
+ const deidentMethod = addDeidentificationMethod(priorMethod, method);
15268
15432
  elements.set(
15269
15433
  TAG_DEIDENTIFICATION_METHOD,
15270
- insertedScalar(TAG_DEIDENTIFICATION_METHOD, "LO", Buffer.from(method, "latin1"), littleEndian)
15434
+ insertedScalar(TAG_DEIDENTIFICATION_METHOD, "LO", deidentMethod.value, littleEndian)
15271
15435
  );
15272
15436
  const warnings = [...processed.warnings];
15437
+ if (deidentMethod.replacedPrior) {
15438
+ warnings.push(
15439
+ deidentMethodNotAdded({ byteOffset: priorMethod?.byteOffset ?? 0, fileMeta: false })
15440
+ );
15441
+ }
15442
+ if (deidentMethod.retainedPrior) {
15443
+ warnings.push(
15444
+ deidentMethodPriorRetained({ byteOffset: priorMethod?.byteOffset ?? 0, fileMeta: false })
15445
+ );
15446
+ }
15273
15447
  if (hasUncleanedBurnedIn(ds)) {
15274
15448
  const offset = ds.get(TAG_PIXEL_DATA)?.byteOffset ?? 0;
15275
15449
  warnings.push(burnedInAnnotationNotRemoved({ byteOffset: offset, fileMeta: false }));