@cosyte/dicom 0.0.10 โ†’ 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.10";
12
+ var VERSION = "0.0.11";
13
13
 
14
14
  // src/dictionary/index.ts
15
15
  var dictionary_exports = {};
@@ -11711,11 +11711,14 @@ function isRecognizedVr(vr) {
11711
11711
  // src/parser/warnings.ts
11712
11712
  var WARNING_CODES = {
11713
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",
11714
11716
  DICOM_EMPTY_ITEM_IN_SEQUENCE: "DICOM_EMPTY_ITEM_IN_SEQUENCE",
11715
11717
  DICOM_FILE_META_GROUP_LENGTH_MISMATCH: "DICOM_FILE_META_GROUP_LENGTH_MISMATCH",
11716
11718
  DICOM_FILE_META_GROUP_LENGTH_MISSING: "DICOM_FILE_META_GROUP_LENGTH_MISSING",
11717
11719
  DICOM_GROUP_LENGTH_IN_DATASET: "DICOM_GROUP_LENGTH_IN_DATASET",
11718
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",
11719
11722
  DICOM_MISSING_PREAMBLE: "DICOM_MISSING_PREAMBLE",
11720
11723
  DICOM_NONZERO_RESERVED_BYTES: "DICOM_NONZERO_RESERVED_BYTES",
11721
11724
  DICOM_ODD_LENGTH_VALUE_PADDED: "DICOM_ODD_LENGTH_VALUE_PADDED",
@@ -11741,6 +11744,10 @@ var WARNING_CODES = {
11741
11744
  // reserved by Phase 7 - not emitted in Phase 2
11742
11745
  DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED: "DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED",
11743
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
11744
11751
  DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE: "DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE",
11745
11752
  // emitted by deidentify(), never by the parser
11746
11753
  DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE: "DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE",
@@ -11765,6 +11772,36 @@ var WARNING_MESSAGES = Object.freeze({
11765
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.",
11766
11773
  DICOM_UN_PARSED_AS_SQ: "Element ({tag}) has VR=UN with undefined length; descended as Implicit VR LE sequence per CP-246.",
11767
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.",
11768
11805
  DICOM_PIXEL_DATA_LENGTH_MISMATCH: "(7FE0,0010) PixelData declared length {n} does not match computed {n2} bytes.",
11769
11806
  DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR: "Private element ({tag}) under Implicit VR LE has no VR override; falling back to UN.",
11770
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.",
@@ -11849,9 +11886,21 @@ function unParsedAsSQ(position, tag) {
11849
11886
  function sqNotDescended(position, tag) {
11850
11887
  return build(WARNING_CODES.DICOM_SQ_NOT_DESCENDED, position, { tag });
11851
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
+ }
11852
11895
  function emptyItemInSequence(position, tag) {
11853
11896
  return build(WARNING_CODES.DICOM_EMPTY_ITEM_IN_SEQUENCE, position, { tag });
11854
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
+ }
11855
11904
  function implicitVRForPrivateTagWithoutVR(position, tag) {
11856
11905
  return build(WARNING_CODES.DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR, position, { tag });
11857
11906
  }
@@ -11861,6 +11910,12 @@ function privateCreatorUnknown(position, tag) {
11861
11910
  function burnedInAnnotationNotRemoved(position) {
11862
11911
  return build(WARNING_CODES.DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED, position);
11863
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
+ }
11864
11919
  function embeddedAttributeRemoved(position, tag, vr, count) {
11865
11920
  return build(WARNING_CODES.DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED, position, {
11866
11921
  tag,
@@ -12155,6 +12210,16 @@ function parseFileMeta(buffer, datasetStart, ctx, emit) {
12155
12210
  fmElements.push(next);
12156
12211
  }
12157
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
+ }
12158
12223
  const tsElement = fmElements.find((e) => e.tag === "00020010");
12159
12224
  if (tsElement === void 0 || tsElement.vr !== "UI") {
12160
12225
  throw new DicomParseError(
@@ -12201,7 +12266,7 @@ function readExplicitLeElement(cursor) {
12201
12266
  if (bytesConsumed !== headerLength + length) {
12202
12267
  throw new RangeError("File Meta element accounting mismatch");
12203
12268
  }
12204
- return { tag, vr, value, bytesConsumed };
12269
+ return { tag, vr, value, bytesConsumed, byteOffset: headerStart };
12205
12270
  }
12206
12271
  function trimUI(buf) {
12207
12272
  let end = buf.length;
@@ -12641,6 +12706,14 @@ var Element = class {
12641
12706
  }
12642
12707
  };
12643
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
+
12644
12717
  // src/dataset/item.ts
12645
12718
  var Item = class extends Dataset {
12646
12719
  index;
@@ -12762,6 +12835,15 @@ function parseSequence(buffer, valueStart, ctx, emit, opts) {
12762
12835
  items.push(new Item({ warnings: [], elements: inner.elements, index: itemIndex }));
12763
12836
  cursor.position = inner.endOffset;
12764
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
+ }
12765
12847
  if (cursor.position + itemLength > buffer.length) {
12766
12848
  throw new DicomParseError(
12767
12849
  FATAL_CODES.INVALID_FILE_META,
@@ -12897,8 +12979,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12897
12979
  cursor.position = seq.endOffset;
12898
12980
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(valueRawStart, cursor.position)) : buffer.subarray(valueRawStart, cursor.position);
12899
12981
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
12900
- elements.set(
12901
- tag,
12982
+ defineElement(
12983
+ elements,
12902
12984
  new Element({
12903
12985
  tag,
12904
12986
  vr,
@@ -12909,7 +12991,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12909
12991
  littleEndian: true,
12910
12992
  items: seq.items,
12911
12993
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
12912
- })
12994
+ }),
12995
+ emit
12913
12996
  );
12914
12997
  continue;
12915
12998
  }
@@ -12928,8 +13011,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12928
13011
  cursor.position = cp246.endOffset;
12929
13012
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(valueRawStart, cursor.position)) : buffer.subarray(valueRawStart, cursor.position);
12930
13013
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
12931
- elements.set(
12932
- tag,
13014
+ defineElement(
13015
+ elements,
12933
13016
  new Element({
12934
13017
  tag,
12935
13018
  vr: "SQ",
@@ -12942,7 +13025,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12942
13025
  littleEndian: true,
12943
13026
  items: cp246.items,
12944
13027
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
12945
- })
13028
+ }),
13029
+ emit
12946
13030
  );
12947
13031
  continue;
12948
13032
  }
@@ -12978,8 +13062,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12978
13062
  );
12979
13063
  if (seq.success) {
12980
13064
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
12981
- elements.set(
12982
- tag,
13065
+ defineElement(
13066
+ elements,
12983
13067
  new Element({
12984
13068
  tag,
12985
13069
  vr,
@@ -12990,7 +13074,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12990
13074
  littleEndian: true,
12991
13075
  items: seq.items,
12992
13076
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
12993
- })
13077
+ }),
13078
+ emit
12994
13079
  );
12995
13080
  continue;
12996
13081
  }
@@ -13003,8 +13088,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
13003
13088
  }
13004
13089
  applySpecificCharacterSet(tag, valueSlice, ctx, emit, position);
13005
13090
  const privateCreator = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13006
- elements.set(
13007
- tag,
13091
+ defineElement(
13092
+ elements,
13008
13093
  new Element({
13009
13094
  tag,
13010
13095
  vr,
@@ -13016,7 +13101,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
13016
13101
  littleEndian: true,
13017
13102
  ...ctx.currentCharset !== void 0 ? { specificCharacterSet: ctx.currentCharset } : {},
13018
13103
  ...privateCreator !== void 0 ? { privateCreator } : {}
13019
- })
13104
+ }),
13105
+ emit
13020
13106
  );
13021
13107
  }
13022
13108
  return { elements, endOffset: cursor.position };
@@ -13123,8 +13209,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13123
13209
  cursor.position = seqResult.endOffset;
13124
13210
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, cursor.position)) : buffer.subarray(headerStart, cursor.position);
13125
13211
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13126
- elements.set(
13127
- tag,
13212
+ defineElement(
13213
+ elements,
13128
13214
  new Element({
13129
13215
  tag,
13130
13216
  vr: "SQ",
@@ -13135,7 +13221,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13135
13221
  littleEndian: mode.littleEndian,
13136
13222
  items: seqResult.items,
13137
13223
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
13138
- })
13224
+ }),
13225
+ emit
13139
13226
  );
13140
13227
  continue;
13141
13228
  }
@@ -13150,8 +13237,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13150
13237
  const result = parseSequence(buffer, valueStart2, ctx, emit, seqOpts);
13151
13238
  cursor.position = result.endOffset;
13152
13239
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, cursor.position)) : buffer.subarray(headerStart, cursor.position);
13153
- elements.set(
13154
- tag,
13240
+ defineElement(
13241
+ elements,
13155
13242
  new Element({
13156
13243
  tag,
13157
13244
  vr: "OB",
@@ -13162,7 +13249,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13162
13249
  byteOffset: headerStart,
13163
13250
  littleEndian: mode.littleEndian,
13164
13251
  items: result.items
13165
- })
13252
+ }),
13253
+ emit
13166
13254
  );
13167
13255
  continue;
13168
13256
  }
@@ -13181,8 +13269,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13181
13269
  cursor.position = cp246.endOffset;
13182
13270
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, cursor.position)) : buffer.subarray(headerStart, cursor.position);
13183
13271
  const privateCreator3 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13184
- elements.set(
13185
- tag,
13272
+ defineElement(
13273
+ elements,
13186
13274
  new Element({
13187
13275
  tag,
13188
13276
  vr: "SQ",
@@ -13195,15 +13283,16 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13195
13283
  littleEndian: mode.littleEndian,
13196
13284
  items: cp246.items,
13197
13285
  ...privateCreator3 !== void 0 ? { privateCreator: privateCreator3 } : {}
13198
- })
13286
+ }),
13287
+ emit
13199
13288
  );
13200
13289
  continue;
13201
13290
  }
13202
13291
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13203
13292
  const fallbackEnd = buffer.length;
13204
13293
  const fallbackBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, fallbackEnd)) : buffer.subarray(headerStart, fallbackEnd);
13205
- elements.set(
13206
- tag,
13294
+ defineElement(
13295
+ elements,
13207
13296
  new Element({
13208
13297
  tag,
13209
13298
  vr: "UN",
@@ -13213,7 +13302,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13213
13302
  byteOffset: headerStart,
13214
13303
  littleEndian: mode.littleEndian,
13215
13304
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
13216
- })
13305
+ }),
13306
+ emit
13217
13307
  );
13218
13308
  cursor.position = fallbackEnd;
13219
13309
  continue;
@@ -13248,8 +13338,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13248
13338
  applySpecificCharacterSet(tag, valueSlice, ctx, emit, position);
13249
13339
  const privateCreator = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13250
13340
  const finalVr = vr;
13251
- elements.set(
13252
- tag,
13341
+ defineElement(
13342
+ elements,
13253
13343
  new Element({
13254
13344
  tag,
13255
13345
  vr: finalVr,
@@ -13261,7 +13351,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13261
13351
  littleEndian: mode.littleEndian,
13262
13352
  ...ctx.currentCharset !== void 0 ? { specificCharacterSet: ctx.currentCharset } : {},
13263
13353
  ...privateCreator !== void 0 ? { privateCreator } : {}
13264
- })
13354
+ }),
13355
+ emit
13265
13356
  );
13266
13357
  }
13267
13358
  return { elements, endOffset: cursor.position };
@@ -15093,6 +15184,19 @@ function itemStreamOverrunsSequence(el, encoding) {
15093
15184
  const headerBytes = encoding === "implicit" ? 0 : EXPLICIT_SQ_HEADER_BYTES;
15094
15185
  return el.rawBytes.length - headerBytes > el.length;
15095
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
+ }
15096
15200
  function keepsPrivate(el, ctx, creators) {
15097
15201
  if (!ctx.active.has("RetainSafePrivate") || ctx.profile === void 0) return false;
15098
15202
  if (isPrivateCreatorElement(el.tag)) {
@@ -15112,10 +15216,11 @@ function processElements(source, ctx, contextPath, reservationsUsable) {
15112
15216
  undefinedVrElements: [],
15113
15217
  warnings: []
15114
15218
  };
15115
- const creators = creatorsInScope(source);
15116
- 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()) {
15117
15222
  if (isPrivateTag(el.tag)) {
15118
- if (reservationsUsable && keepsPrivate(el, ctx, creators))
15223
+ if (reservationsUsable && isSettled(el, at, bound) && keepsPrivate(el, ctx, creators))
15119
15224
  keepOrEmpty(el, ctx, contextPath, out);
15120
15225
  else out.removedPrivateTags.push(el.tag);
15121
15226
  continue;
@@ -15248,6 +15353,47 @@ function defaultMethod(active) {
15248
15353
  const base = "Cosyte @cosyte/dicom: PS3.15 Basic Application Level Confidentiality Profile";
15249
15354
  return active.size === 0 ? base : `${base} + ${[...active].join(", ")}`;
15250
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
+ }
15251
15397
  function hasUncleanedBurnedIn(ds) {
15252
15398
  if (!ds.has(TAG_PIXEL_DATA)) return false;
15253
15399
  const flag = ds.get(TAG_BURNED_IN_ANNOTATION);
@@ -15281,11 +15427,23 @@ function deidentify(ds, options = {}) {
15281
15427
  insertedScalar(TAG_PATIENT_IDENTITY_REMOVED, "CS", Buffer.from("YES", "latin1"), littleEndian)
15282
15428
  );
15283
15429
  const method = options.deidentificationMethod ?? defaultMethod(active);
15430
+ const priorMethod = elements.get(TAG_DEIDENTIFICATION_METHOD);
15431
+ const deidentMethod = addDeidentificationMethod(priorMethod, method);
15284
15432
  elements.set(
15285
15433
  TAG_DEIDENTIFICATION_METHOD,
15286
- insertedScalar(TAG_DEIDENTIFICATION_METHOD, "LO", Buffer.from(method, "latin1"), littleEndian)
15434
+ insertedScalar(TAG_DEIDENTIFICATION_METHOD, "LO", deidentMethod.value, littleEndian)
15287
15435
  );
15288
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
+ }
15289
15447
  if (hasUncleanedBurnedIn(ds)) {
15290
15448
  const offset = ds.get(TAG_PIXEL_DATA)?.byteOffset ?? 0;
15291
15449
  warnings.push(burnedInAnnotationNotRemoved({ byteOffset: offset, fileMeta: false }));