@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/README.md CHANGED
@@ -45,7 +45,7 @@ That's the pitch: no config, no schema upload, no spec lookup. The parser accept
45
45
  - **One-line metadata extraction**: `ds.patient`, `ds.study`, `ds.series`, `ds.image`: typed, fail-safe views over the safety-critical attributes. No `(group,element)` tags to memorise.
46
46
  - **Two access patterns**: named views, or structural `ds.get("00100010")` by 8-character `(group,element)` tag (resolve a keyword to its tag with `Dictionary.byKeyword`), plus `ds.elements()` to walk everything.
47
47
  - **Lazy typed value decode**: `element.value` decodes raw bytes into a discriminated `DicomValue` across all 34 VRs (numbers, `bigint`s, person names, dates/times, sequences, raw `binary`), honoring `(0008,0005)` Specific Character Set through nested items.
48
- - **Real-world tolerance, Postel's Law**: a lenient reader emits 25 stable warning codes for what it tolerated; only 4 truly-structural conditions are fatal. The serializer always writes spec-clean Part 10.
48
+ - **Real-world tolerance, Postel's Law**: a lenient reader emits stable warning codes for what it tolerated; only 4 truly-structural conditions are fatal. The serializer always writes spec-clean Part 10. (The count used to be quoted here and had drifted: it read `25` against a `WARNING_CODES` of `28`. The locked snapshot in `test/property/__snapshots__/warning-codes.snapshot.test.ts.snap` is the pin, and it is measured on every run rather than narrated here.)
49
49
  - **Source/vendor profile system**: `defineProfile()` + 5 built-ins (`ge`, `siemens`, `philips`, `strict`, `lenient`) that only ever _tighten or annotate_ a parse, resolving vendor private tags by the file's live Private Creator string, never a wrong decode.
50
50
  - **Metadata-level de-identification**: `deidentify()` applies the PS3.15 Annex E Basic Profile + the nine metadata Options, returning a fresh dataset and an audit report built from static tables, with two fields that carry source bytes and are named as such on the type: `report.uidMap` (source UIDs) and `report.removedPrivateTags`.
51
51
  - **Spec-clean serializer**: `serializeDicom(ds)` round-trips a dataset back to Part 10 bytes in its source transfer syntax (no transcode), with correct File Meta group length, even-length padding, byte-exact sequence passthrough, and lossless File Meta: non-modeled `(0002,xxxx)` elements are preserved and re-emitted in tag order.
@@ -318,10 +318,14 @@ for (const w of ds.warnings) {
318
318
  }
319
319
  ```
320
320
 
321
- The 26 Tier-2 codes (`DICOM_MISSING_PREAMBLE`, `DICOM_FILE_META_GROUP_LENGTH_MISMATCH`, `DICOM_UN_PARSED_AS_SQ`, `DICOM_ODD_LENGTH_VALUE_PADDED`, `DICOM_PRIVATE_CREATOR_UNKNOWN`, `DICOM_VR_MISMATCH`, `DICOM_DA_LEGACY_FORMAT`, … ) live in [`src/parser/warnings.ts`](./src/parser/warnings.ts). Narrow on `w.code === WARNING_CODES.…` for typo-free comparisons, or pass `{ onWarning }` to `parseDicom` to stream them.
321
+ The Tier-2 codes (`DICOM_MISSING_PREAMBLE`, `DICOM_FILE_META_GROUP_LENGTH_MISMATCH`, `DICOM_UN_PARSED_AS_SQ`, `DICOM_ODD_LENGTH_VALUE_PADDED`, `DICOM_PRIVATE_CREATOR_UNKNOWN`, `DICOM_VR_MISMATCH`, `DICOM_DA_LEGACY_FORMAT`, … ) live in [`src/parser/warnings.ts`](./src/parser/warnings.ts), which is the only place their number is worth reading: it said 26 here while the registry held 29, so the numeral is gone rather than corrected. Narrow on `w.code === WARNING_CODES.…` for typo-free comparisons, or pass `{ onWarning }` to `parseDicom` to stream them.
322
322
 
323
323
  The 4 Tier-3 fatal codes (`NOT_DICOM_PART_10`, `INVALID_FILE_META`, `UNSUPPORTED_TRANSFER_SYNTAX`, `EMPTY_INPUT`) always throw a `DicomParseError`; they represent input the parser cannot meaningfully recover.
324
324
 
325
+ **Two Tier-2 codes report a LOSS rather than a tolerated deviation, and they are worth reading before you trust a parsed object.** A parsed Data Set is a map keyed by tag, so a file that carries one tag twice in the same Data Set loses the first element's value: the second replaces it, and the survivor looks exactly like an element the sender wrote once. `DICOM_DUPLICATE_TAG_IN_DATA_SET` is raised at the moment of the replacement, with the byte offset of the header that replaced. **That offset is the surviving element's own `Element.byteOffset`, which makes it a lookup only for a collision at the root** - inside a defined-length Sequence Item `Element.byteOffset` is relative to that item's own slice, so the same number can name an untouched root element and the warning does not tell you which Data Set it came from (`position.contextPath` is not populated by any parser warning). PS3.5 2026c §7.1 requires a tag to occur at most once in a Data Set and §7.5.1 requires the same inside an Item, so it cannot fire on a conformant file - the ordinary way to reach it is a length field that lies, which makes bytes inside somebody's value read as a Data Element header. **The reading is unchanged: last one read still wins, and nothing is guessed for the value that was replaced.** If you see this code, the object is missing something the file contained and no round trip will show you what; treat it as you would a fatal, and raise the file with the sender.
326
+
327
+ **`DICOM_DUPLICATE_FILE_META_ELEMENT` is the same loss in the `(0002,xxxx)` group, and that group is the one that decides how every following byte is read.** The File Meta group is collected into an array rather than a map, so nothing is overwritten there - but the eight tags this library projects into typed `FileMeta` fields are answered by a **first-match** search and are excluded from `FileMeta.extraElements`, the verbatim residue that gives the group its byte-exact round trip. A second copy of one of those tags is in neither, so it left the object. Two copies of `(0002,0010)` Transfer Syntax UID carrying different UIDs are two different readings of the same file, and the order alone decides which you get. **The two codes resolve a repeat the opposite way round, deliberately, because the two readings do: the FIRST copy wins in the File Meta group, the LAST read wins in a Data Set.** Neither reading changed in this release. Unlike the Data Set code, `position.byteOffset` here is unambiguously file-absolute - the File Meta group is never nested - and it locates the copy that was **dropped**, not the survivor. A repeated `(0002,xxxx)` tag this library does not model is silent, because every copy of one is kept in `extraElements` and nothing is dropped - though note that `serializeDicom` then **re-emits both copies**, which is `PRE-EXISTING` and is where this package's round-trip promise and its spec-clean promise disagree. **Two bounds, both `PRE-EXISTING` and neither closed here.** The disclosure covers the group **as the parser delimits it**: a copy an intermediary appended past an honest `(0002,0000)` group length is never a File Meta element to this parser at all, and is relocated into the main Data Set silently. And an over-long or wrong group length is reported by its own codes, not this one.
328
+
325
329
  ---
326
330
 
327
331
  ## Error Handling
@@ -366,8 +370,10 @@ Thrown by `serializeDicom` for `MISSING_TRANSFER_SYNTAX` (the dataset names no t
366
370
  - **Burned-in PHI.** v1 **warns** it cannot remove burned-in annotation; a "de-identified" output is **metadata-de-identified only**.
367
371
  - **A sequence the parser could not open: content is dropped, not passed through.** `deidentify()` recurses only into a sequence whose items the parser materialized. A **standard** `SQ` element with no items is now **emptied**: its bytes are Data Sets by PS3.5 §7.5.1 and PS3.15 §E.1.1 obliges the de-identifier to reach the listed attributes inside them, so a run that cannot enumerate them must not ship them. `report.unauditableSequences` names the carrier and the byte length dropped (capped at 64 entries; the emptying itself is never capped), and `DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE` is raised. Expect **data loss** on such a file. The parse refusal is usually a sender-side encoding defect, but not always: a **conformant** file nested deeper than this library's own `NESTING_DEPTH_LIMIT` of 64 is refused the same way and loses that sequence too. **Two shapes are exempt and still leak.** A **private** `SQ` kept under `RetainSafePrivate` + a `Profile` is kept verbatim and unaudited, because the profile vouched for it. And an undefined-length **`UN`** the CP-246 descent could not read as a sequence keeps `vr === "UN"`, so the rule does not reach it and cannot be extended there: every ordinary `UN` element also has no items. For either, the reliable test remains `el.items === undefined` on the element itself.
368
372
  - **An unrecognized Explicit VR is read (and written) long-form, and `deidentify()` still empties the element.** PS3.5 2026c §6.2: "All new VRs defined in future versions of DICOM **shall** be of the same Data Element Structure as defined in [§7.1.2] with reserved bytes after the VR and a 32-bit unsigned integer VL." So two on-wire VR bytes outside the 34 this release knows get a **12-byte header**, exactly as `OB` and `UT` do, on both the read and the write path. Before `0.0.9` the reader took the 8-byte form, which read the length out of the two bytes §6.2 reserves. What such a file did then was **shape-specific** - sometimes a whole-object refusal, sometimes a clean parse into a tree the sender did not write - which is why this package ships `scripts/measure-unrecognized-vr.ts` and no one-sentence account of it. The value is otherwise handled like any other: a declared length past the end of the buffer, or an undefined length, is refused exactly as it is for every other non-`SQ` VR. **The trade is stated rather than buried**: a sender that ignores §6.2 and writes an unrecognized VR short-form produced a readable object before and is refused now. `deidentify()` **still empties** such an element, because reading a header is not the same as knowing what its value means - Table E.1-1 acts per attribute, and nothing can say whether a VR from a later edition holds a name, a date or an opaque blob. `report.undefinedVrElements` names the **byte offset** and the byte length dropped (capped at 64 entries; the emptying itself is never capped) and `DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE` is raised. It names **no tag**, deliberately and uniquely among the report's findings: the header may still have been fabricated out of the middle of some element's value, in which case its tag bytes are document content - and nothing distinguishes that case from an honestly-written unrecognized VR, so the tag is withheld either way. An Implicit VR LE file cannot reach any of this: there the VR comes from the dictionary. `UN` is one of the 34 and is not affected. **There is no exemption** - unlike the sequence rule above, `RetainSafePrivate` does not keep such an element.
369
- - **`RetainSafePrivate` retains nothing inside ANY Item of a sequence whose item stream over-runs its own declared length.** An Item that declares more bytes than its enclosing Sequence does absorbs whatever follows the sequence, so a private data element the sender wrote at the **root** - with no Private Creator reserving its block there - can land beside a creator that is genuine Item content and be kept verbatim. Through `0.0.9` it was, with `report.removedPrivateTags` reading `[]` and the object stamped `(0012,0062) Patient Identity Removed = YES`: a stamp that outran the redaction. PS3.5 2026c §7.8.1 scopes a private block reservation to the Item ("The scope of the reservation is just within the Item"), so a file that contradicts itself about where the Item ends does not determine which reservation covers an element; PS3.15 2026c §E.3.10 retains only what is **known** safe and requires that "all other Private Attributes shall be removed **or processed in the element-specific manner recommended by Deidentification Action (0008,0307), if present within Private Data Element Characteristics Sequence (0008,0300)**" (a two-branch clause; this library does not implement `(0008,0307)`, so removal is the branch available to it). So every private element the recursion **reaches** inside such a sequence, at every depth below it, is removed and named in `report.removedPrivateTags`. **The unit is the SEQUENCE, not the individual Item**: `descendSequence` decides once and applies it to every item, so a two-item sequence where only item 1 over-runs also loses a genuine block sitting in the honest item 0 (measured; the grid sweeps single-item fixtures, so that extra cost is NOT inside the priced 20). **Two routes are not closed and both still leak, measured and pinned:** an Item that _under_-declares **ejects** its trailing elements out into the enclosing Data Set, which is not narrowed - **at any depth, not only at the root** (the widening was built and measured at 24 retentions lost to close 2 of 22, so it is a separate slice); and a **private `SQ`** the profile vouches for is kept verbatim before the descent runs, which is the carve-out named two bullets above. **The direction is not fail-safe on its own and the opposite claim is retracted**: it depends on where the sender put the Private Creator. Both **absorb** placements are pinned by tests; the **eject** placement is pinned as a residual and still leaks. **The price is measured, not asserted**: on `scripts/measure-sq-bound-grid.ts`, 58 cells lose a private value and **20 of them were not leaking anything** (the creator and the data element were both genuine Item content). Retention on a file that does not contradict itself is unchanged, and **no reading changes** - no parser file is touched. This is not a parser bound: an over-declaring Item and an under-declaring Sequence are byte-identical, so nothing on the wire can separate them.
373
+ - **`RetainSafePrivate` retains nothing inside ANY Item of a sequence whose item stream over-runs its own declared length.** An Item that declares more bytes than its enclosing Sequence does absorbs whatever follows the sequence, so a private data element the sender wrote at the **root** - with no Private Creator reserving its block there - can land beside a creator that is genuine Item content and be kept verbatim. Measured on the published `0.0.8` tarball it was, with `report.removedPrivateTags` reading `[]`, the object stamped `(0012,0062) Patient Identity Removed = YES` and `ds.warnings` empty: a stamp that outran the redaction. (**There is no `0.0.9` on the registry** - `package.json` carried it and the publish never happened, an npm `E404` - so earlier wording here that said "through `0.0.9`" named a version that does not exist. Measured on the current published `0.0.10`, this direction is fixed.) PS3.5 2026c §7.8.1 scopes a private block reservation to the Item ("The scope of the reservation is just within the Item"), so a file that contradicts itself about where the Item ends does not determine which reservation covers an element; PS3.15 2026c §E.3.10 retains only what is **known** safe and requires that "all other Private Attributes shall be removed **or processed in the element-specific manner recommended by Deidentification Action (0008,0307), if present within Private Data Element Characteristics Sequence (0008,0300)**" (a two-branch clause; this library does not implement `(0008,0307)`, so removal is the branch available to it). So every private element the recursion **reaches** inside such a sequence, at every depth below it, is removed and named in `report.removedPrivateTags`. **The unit is the SEQUENCE, not the individual Item**: `descendSequence` decides once and applies it to every item, so a two-item sequence where only item 1 over-runs also loses a genuine block sitting in the honest item 0 (measured; the grid sweeps single-item fixtures, so that extra cost is NOT inside the priced 20). **One route is not closed and still leaks, measured and pinned:** a **private `SQ`** the profile vouches for is kept verbatim before the descent runs, which is the carve-out named two bullets above. **The direction is not fail-safe on its own and the opposite claim is retracted**: it depends on where the sender put the Private Creator. Both **absorb** placements are pinned by tests, and so is the **eject** placement (below). Treat `DICOM_ITEM_CROSSES_SEQUENCE_END` as "this file's structure is in dispute", never as a statement about what was retained. **The price is measured, not asserted**: on `scripts/measure-sq-bound-grid.ts`, 58 cells lose a private value and **20 of them were not leaking anything** (the creator and the data element were both genuine Item content). Retention on a file that does not contradict itself is unchanged, and **no reading changes** - no parser file is touched. This is not a parser bound: an over-declaring Item and an under-declaring Sequence are byte-identical, so nothing on the wire can separate them.
374
+ - **`RetainSafePrivate` also retains nothing a Data Set holds AFTER a sequence whose own contents contradict the extent it declared.** The mirror direction: an Item that _under_-declares **ejects** its trailing elements out into the enclosing Data Set, and a Private Creator that lands there reserves a block for elements the sender never put beside it - the same false attestation. **Measured on the published `0.0.10` tarball, which is the current registry version**: `report.removedPrivateTags` `[]`, the value in the serialized output, `(0012,0062) = YES`, `ds.warnings` empty and no throw under `{ strict: true }`. It applies **in every Data Set, not only the root**: an inner sequence ejecting a creator into the still-usable Item that encloses it is measured and pinned. The cut is **positional**, so a reservation the sender wrote **ahead of** the offending sequence is untouched, and the same reservation written **after** it is refused. **Two predicates, because the parser records the same contradiction two ways**: under Explicit VR the item stream over-runs and the span shows up as `rawBytes.length > length`; under Implicit VR LE that path slices the item stream, so nothing over-runs at all and the descent is refused instead (`DICOM_SQ_NOT_DESCENDED`, `Element.items === undefined`). The second predicate is **broader than the ejection it is here for, deliberately**: it says the parser could not walk the sequence, which other unwalkable item streams also reach. **And two BOUNDS, not one, because a Data Set is a `Map<Tag, Element>`**: an ejected element whose tag the Data Set already holds overwrites in place and inherits the earlier element's position, ahead of the sequence it came out of, so `Element.byteOffset` is checked beside the index. **That collision also destroys the root's own value at parse time** - `PRE-EXISTING`, unchanged here, and pinned by a test so it is not mistaken for something this rule handles. It was silent when that was written; `DICOM_DUPLICATE_TAG_IN_DATA_SET` reports it now (below), and the destruction itself is still unchanged. **The price is measured**: on `scripts/measure-sq-bound-grid.ts` all **22** cells that retained a private value at the root on a self-contradicting file whose honest control removes it go to **0**, at the cost of **56** further root retentions on self-contradicting files whose honest control does keep the value (the column is `de-identified OUTPUT lost a marker (cost)`, reading 78; the parse-tree `LOST`/`GAINED a marker value` counters beside it read 0). Retention on a file that does not contradict itself is unchanged (9 at the root, 6 in an Item), and **no reading changes**: 0 cells differ in any parse respect, 0 cells whose reading differs, and no parser file is touched.
370
375
  - **An over-declared Value Length into a binary carrier still leaks, and it is a known trade.** The mirror case - an _over_-declared length that swallows the following element into an `OB`/`OW`/`US`/`UN` value - is **not** detected, and a `(0010,0020)` inside it reaches de-identified output with no warning and no report entry. Arbitrary bytes are exactly what those VRs are for, so no content test can decide it; the only candidate remedy was measured and empties conformant binary values (a legal LUT or blob deleted because 8 of its bytes read as a zero-length `(0010,0020)`). String carriers **are** covered, because there the same bytes are provably outside the VR's repertoire. If you accept files from a sender you do not control, treat a binary attribute's length as untrusted.
376
+ - **`(0012,0063)` De-identification Method is ADDED TO, so a prior de-identifier's text is carried into the output.** PS3.15 2026c §E.1.1 says a text string describing the method "shall be inserted in or added to" that attribute, and this library used to **replace** it - destroying the provenance chain the attribute exists to carry, silently, on a file whose earlier pass may be the one a recipient was relying on. It now appends its own text as a further value of the `1-n` attribute, after a `\`, copying the prior bytes through verbatim. **The method string is itself a `1-n` value**: it is split on `\` and only the values not already recorded are added. **Trailing SPACE and NUL are padding rather than content** (PS3.5 2026c Table 6.2-1's `LO` row, which describes a **Value**, and `LO` is `1-n`), so they are ignored **per value, on both sides** when your string is matched against what is already recorded, and trimmed from the value written. Repeated de-identification is therefore a fixed point **from the first pass**, for every string: with or without a delimiter, and with a pad byte on any value, last or not. Leading spaces are yours and are written through untouched; a method string that is padding only records nothing. **One bound, and it is not cosmetic**: `LO` is a short-form VR whose Value Length field is 16 bits, so when the join would not fit, the prior value is **replaced** rather than added to and `report.warnings` carries `DICOM_DEIDENT_METHOD_NOT_ADDED`. Read that code as "the length ceiling was reached", never as "every fallback is disclosed": a `(0012,0063)` a file encoded under a VR other than `LO` is also replaced, and that one is silent (`PRE-EXISTING`). An element the serializer cannot encode would take the whole de-identified object down, and truncating the sender's earlier records instead would be a policy the standard does not state. A `deidentificationMethod` longer than that ceiling on its own is unchanged from every earlier release and still fails to serialize. **The cost is stated rather than glossed**: `(0012,0063)` is **not in Table E.1-1**, so the Basic Profile keeps it exactly as it keeps every other unlisted attribute, and a sender who wrote something identifying into it now sees that text in de-identified output where the replacement used to remove it as a side effect. That is the retained-by-omission posture of every unlisted attribute, not a rule this insertion states; it is pinned by a residual test that asserts the cost. **It is not silent**: `report.warnings` carries `DICOM_DEIDENT_METHOD_PRIOR_RETAINED` whenever prior bytes from the source file survive into `(0012,0063)`, because an object stamped `(0012,0062) = YES` carrying text nothing in the run inspected is a stamp that outran the redaction. It is **not** on `report.retained` - that field lists the Annex E option sets active for the run, and a kept attribute is not one of them. `(0012,0062)` Patient Identity Removed is still **replaced** with `YES` - the same paragraph of §E.1.1 uses the other verb for it ("shall be replaced or added to"), and the asymmetry is the standard's. A `(0012,0063)` a file encoded under a VR other than `LO` is replaced rather than appended to.
371
377
  - **Networking & web.** No DIMSE (C-STORE/FIND/MOVE, MWL, MPPS); no DICOMweb (QIDO/WADO/STOW). → `@cosyte/dicom-net`, `@cosyte/dicomweb`.
372
378
  - **Transcoding.** No transfer-syntax conversion. The serializer re-emits in the dataset's source syntax only.
373
379
  - **Terminology resolution.** Coded values are surfaced (designator + canonical source) but not validated against SNOMED/LOINC/etc.
package/dist/index.cjs CHANGED
@@ -11,7 +11,7 @@ var __export = (target, all) => {
11
11
  };
12
12
 
13
13
  // src/version.ts
14
- var VERSION = "0.0.10";
14
+ var VERSION = "0.0.11";
15
15
 
16
16
  // src/dictionary/index.ts
17
17
  var dictionary_exports = {};
@@ -11713,11 +11713,14 @@ function isRecognizedVr(vr) {
11713
11713
  // src/parser/warnings.ts
11714
11714
  var WARNING_CODES = {
11715
11715
  // === Phase 2 actively emits (D-08 active list - alphabetical-within-prefix per CONTEXT specifics §) ===
11716
+ DICOM_DUPLICATE_FILE_META_ELEMENT: "DICOM_DUPLICATE_FILE_META_ELEMENT",
11717
+ DICOM_DUPLICATE_TAG_IN_DATA_SET: "DICOM_DUPLICATE_TAG_IN_DATA_SET",
11716
11718
  DICOM_EMPTY_ITEM_IN_SEQUENCE: "DICOM_EMPTY_ITEM_IN_SEQUENCE",
11717
11719
  DICOM_FILE_META_GROUP_LENGTH_MISMATCH: "DICOM_FILE_META_GROUP_LENGTH_MISMATCH",
11718
11720
  DICOM_FILE_META_GROUP_LENGTH_MISSING: "DICOM_FILE_META_GROUP_LENGTH_MISSING",
11719
11721
  DICOM_GROUP_LENGTH_IN_DATASET: "DICOM_GROUP_LENGTH_IN_DATASET",
11720
11722
  DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR: "DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR",
11723
+ DICOM_ITEM_CROSSES_SEQUENCE_END: "DICOM_ITEM_CROSSES_SEQUENCE_END",
11721
11724
  DICOM_MISSING_PREAMBLE: "DICOM_MISSING_PREAMBLE",
11722
11725
  DICOM_NONZERO_RESERVED_BYTES: "DICOM_NONZERO_RESERVED_BYTES",
11723
11726
  DICOM_ODD_LENGTH_VALUE_PADDED: "DICOM_ODD_LENGTH_VALUE_PADDED",
@@ -11743,6 +11746,10 @@ var WARNING_CODES = {
11743
11746
  // reserved by Phase 7 - not emitted in Phase 2
11744
11747
  DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED: "DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED",
11745
11748
  // emitted by deidentify(), never by the parser
11749
+ DICOM_DEIDENT_METHOD_NOT_ADDED: "DICOM_DEIDENT_METHOD_NOT_ADDED",
11750
+ // emitted by deidentify(), never by the parser
11751
+ DICOM_DEIDENT_METHOD_PRIOR_RETAINED: "DICOM_DEIDENT_METHOD_PRIOR_RETAINED",
11752
+ // emitted by deidentify(), never by the parser
11746
11753
  DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE: "DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE",
11747
11754
  // emitted by deidentify(), never by the parser
11748
11755
  DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE: "DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE",
@@ -11767,6 +11774,36 @@ var WARNING_MESSAGES = Object.freeze({
11767
11774
  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.",
11768
11775
  DICOM_UN_PARSED_AS_SQ: "Element ({tag}) has VR=UN with undefined length; descended as Implicit VR LE sequence per CP-246.",
11769
11776
  DICOM_EMPTY_ITEM_IN_SEQUENCE: "Sequence ({tag}) contains an empty item (length=0); tolerated.",
11777
+ // Neither the tag nor the replaced element's value is here, and the tag's
11778
+ // absence is specific to this code rather than caution. See
11779
+ // `duplicateTagInDataSet`. Short by measurement rather than by intention: one
11780
+ // is raised per collision and the collision count is chosen by the input, so a
11781
+ // 400-character draft was measured at 50 M characters over a 1 MiB file and
11782
+ // cut. The reasoning lives in the factory's JSDoc, not in the string.
11783
+ 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.",
11784
+ // The tag is deliberately absent for the same reason as the code above, and
11785
+ // the surviving copy is named because the File Meta group resolves a repeat
11786
+ // the OPPOSITE way round to a Data Set: first copy wins here, last read wins
11787
+ // there. Short for the same measured reason - one is raised per repeat and the
11788
+ // repeat count is chosen by the input.
11789
+ // 🛑 NO SPEC CITATION HERE, AND ITS ABSENCE IS DELIBERATE. A draft ended this
11790
+ // string "PS3.5 7.1.", which asserts that section's "at most once in a Data
11791
+ // Set" governs the File Meta group - the applicability this slice states it
11792
+ // cannot establish, because PS3.10 governs that group and is not vendored
11793
+ // here. Under `{ strict: true }` this string is also the thrown message, so an
11794
+ // unqualified citation there is the claim, not a footnote. Do not put it back.
11795
+ 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.",
11796
+ // No value and no length: the prior value is the file's own text. The tag is a
11797
+ // constant of this code rather than composed from input, as in the two
11798
+ // (0002,0000) messages above.
11799
+ 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.",
11800
+ // No value and no length, for the same reason as the code above: the retained
11801
+ // text is the file's own. The tag is a constant of this code.
11802
+ 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.",
11803
+ // The Item's own declared length is deliberately absent. See
11804
+ // `itemCrossesSequenceEnd`; `{n2}` stays because the emit site's
11805
+ // `endLimit < buffer.length` conjunct bounds it by the buffer.
11806
+ 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.",
11770
11807
  DICOM_PIXEL_DATA_LENGTH_MISMATCH: "(7FE0,0010) PixelData declared length {n} does not match computed {n2} bytes.",
11771
11808
  DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR: "Private element ({tag}) under Implicit VR LE has no VR override; falling back to UN.",
11772
11809
  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.",
@@ -11851,9 +11888,21 @@ function unParsedAsSQ(position, tag) {
11851
11888
  function sqNotDescended(position, tag) {
11852
11889
  return build(WARNING_CODES.DICOM_SQ_NOT_DESCENDED, position, { tag });
11853
11890
  }
11891
+ function itemCrossesSequenceEnd(position, tag, availableLength) {
11892
+ return build(WARNING_CODES.DICOM_ITEM_CROSSES_SEQUENCE_END, position, {
11893
+ tag,
11894
+ n2: availableLength
11895
+ });
11896
+ }
11854
11897
  function emptyItemInSequence(position, tag) {
11855
11898
  return build(WARNING_CODES.DICOM_EMPTY_ITEM_IN_SEQUENCE, position, { tag });
11856
11899
  }
11900
+ function duplicateTagInDataSet(position) {
11901
+ return build(WARNING_CODES.DICOM_DUPLICATE_TAG_IN_DATA_SET, position);
11902
+ }
11903
+ function duplicateFileMetaElement(position) {
11904
+ return build(WARNING_CODES.DICOM_DUPLICATE_FILE_META_ELEMENT, position);
11905
+ }
11857
11906
  function implicitVRForPrivateTagWithoutVR(position, tag) {
11858
11907
  return build(WARNING_CODES.DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR, position, { tag });
11859
11908
  }
@@ -11863,6 +11912,12 @@ function privateCreatorUnknown(position, tag) {
11863
11912
  function burnedInAnnotationNotRemoved(position) {
11864
11913
  return build(WARNING_CODES.DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED, position);
11865
11914
  }
11915
+ function deidentMethodNotAdded(position) {
11916
+ return build(WARNING_CODES.DICOM_DEIDENT_METHOD_NOT_ADDED, position);
11917
+ }
11918
+ function deidentMethodPriorRetained(position) {
11919
+ return build(WARNING_CODES.DICOM_DEIDENT_METHOD_PRIOR_RETAINED, position);
11920
+ }
11866
11921
  function embeddedAttributeRemoved(position, tag, vr, count) {
11867
11922
  return build(WARNING_CODES.DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED, position, {
11868
11923
  tag,
@@ -12157,6 +12212,16 @@ function parseFileMeta(buffer$1, datasetStart, ctx, emit) {
12157
12212
  fmElements.push(next);
12158
12213
  }
12159
12214
  }
12215
+ const seenModeledTags = /* @__PURE__ */ new Set();
12216
+ if (declaredFmLength !== void 0) seenModeledTags.add("00020000");
12217
+ for (const e of fmElements) {
12218
+ if (!MODELED_FM_TAGS.has(e.tag)) continue;
12219
+ if (seenModeledTags.has(e.tag)) {
12220
+ emit(duplicateFileMetaElement({ byteOffset: e.byteOffset, fileMeta: true }));
12221
+ continue;
12222
+ }
12223
+ seenModeledTags.add(e.tag);
12224
+ }
12160
12225
  const tsElement = fmElements.find((e) => e.tag === "00020010");
12161
12226
  if (tsElement === void 0 || tsElement.vr !== "UI") {
12162
12227
  throw new DicomParseError(
@@ -12203,7 +12268,7 @@ function readExplicitLeElement(cursor) {
12203
12268
  if (bytesConsumed !== headerLength + length) {
12204
12269
  throw new RangeError("File Meta element accounting mismatch");
12205
12270
  }
12206
- return { tag, vr, value, bytesConsumed };
12271
+ return { tag, vr, value, bytesConsumed, byteOffset: headerStart };
12207
12272
  }
12208
12273
  function trimUI(buf) {
12209
12274
  let end = buf.length;
@@ -12643,6 +12708,14 @@ var Element = class {
12643
12708
  }
12644
12709
  };
12645
12710
 
12711
+ // src/parser/data-set-map.ts
12712
+ function defineElement(elements, element, emit) {
12713
+ if (elements.has(element.tag)) {
12714
+ emit(duplicateTagInDataSet({ byteOffset: element.byteOffset }));
12715
+ }
12716
+ elements.set(element.tag, element);
12717
+ }
12718
+
12646
12719
  // src/dataset/item.ts
12647
12720
  var Item = class extends Dataset {
12648
12721
  index;
@@ -12764,6 +12837,15 @@ function parseSequence(buffer, valueStart, ctx, emit, opts) {
12764
12837
  items.push(new Item({ warnings: [], elements: inner.elements, index: itemIndex }));
12765
12838
  cursor.position = inner.endOffset;
12766
12839
  } else {
12840
+ if (opts.explicitLength !== void 0 && endLimit < buffer.length && cursor.position + itemLength > endLimit) {
12841
+ emit(
12842
+ itemCrossesSequenceEnd(
12843
+ { byteOffset: itemHeaderStart },
12844
+ itemTag,
12845
+ Math.max(0, endLimit - cursor.position)
12846
+ )
12847
+ );
12848
+ }
12767
12849
  if (cursor.position + itemLength > buffer.length) {
12768
12850
  throw new DicomParseError(
12769
12851
  FATAL_CODES.INVALID_FILE_META,
@@ -12899,8 +12981,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12899
12981
  cursor.position = seq.endOffset;
12900
12982
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(valueRawStart, cursor.position)) : buffer.subarray(valueRawStart, cursor.position);
12901
12983
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
12902
- elements.set(
12903
- tag,
12984
+ defineElement(
12985
+ elements,
12904
12986
  new Element({
12905
12987
  tag,
12906
12988
  vr,
@@ -12911,7 +12993,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12911
12993
  littleEndian: true,
12912
12994
  items: seq.items,
12913
12995
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
12914
- })
12996
+ }),
12997
+ emit
12915
12998
  );
12916
12999
  continue;
12917
13000
  }
@@ -12930,8 +13013,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12930
13013
  cursor.position = cp246.endOffset;
12931
13014
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(valueRawStart, cursor.position)) : buffer.subarray(valueRawStart, cursor.position);
12932
13015
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
12933
- elements.set(
12934
- tag,
13016
+ defineElement(
13017
+ elements,
12935
13018
  new Element({
12936
13019
  tag,
12937
13020
  vr: "SQ",
@@ -12944,7 +13027,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12944
13027
  littleEndian: true,
12945
13028
  items: cp246.items,
12946
13029
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
12947
- })
13030
+ }),
13031
+ emit
12948
13032
  );
12949
13033
  continue;
12950
13034
  }
@@ -12980,8 +13064,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12980
13064
  );
12981
13065
  if (seq.success) {
12982
13066
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
12983
- elements.set(
12984
- tag,
13067
+ defineElement(
13068
+ elements,
12985
13069
  new Element({
12986
13070
  tag,
12987
13071
  vr,
@@ -12992,7 +13076,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12992
13076
  littleEndian: true,
12993
13077
  items: seq.items,
12994
13078
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
12995
- })
13079
+ }),
13080
+ emit
12996
13081
  );
12997
13082
  continue;
12998
13083
  }
@@ -13005,8 +13090,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
13005
13090
  }
13006
13091
  applySpecificCharacterSet(tag, valueSlice, ctx, emit, position);
13007
13092
  const privateCreator = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13008
- elements.set(
13009
- tag,
13093
+ defineElement(
13094
+ elements,
13010
13095
  new Element({
13011
13096
  tag,
13012
13097
  vr,
@@ -13018,7 +13103,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
13018
13103
  littleEndian: true,
13019
13104
  ...ctx.currentCharset !== void 0 ? { specificCharacterSet: ctx.currentCharset } : {},
13020
13105
  ...privateCreator !== void 0 ? { privateCreator } : {}
13021
- })
13106
+ }),
13107
+ emit
13022
13108
  );
13023
13109
  }
13024
13110
  return { elements, endOffset: cursor.position };
@@ -13125,8 +13211,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13125
13211
  cursor.position = seqResult.endOffset;
13126
13212
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, cursor.position)) : buffer.subarray(headerStart, cursor.position);
13127
13213
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13128
- elements.set(
13129
- tag,
13214
+ defineElement(
13215
+ elements,
13130
13216
  new Element({
13131
13217
  tag,
13132
13218
  vr: "SQ",
@@ -13137,7 +13223,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13137
13223
  littleEndian: mode.littleEndian,
13138
13224
  items: seqResult.items,
13139
13225
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
13140
- })
13226
+ }),
13227
+ emit
13141
13228
  );
13142
13229
  continue;
13143
13230
  }
@@ -13152,8 +13239,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13152
13239
  const result = parseSequence(buffer, valueStart2, ctx, emit, seqOpts);
13153
13240
  cursor.position = result.endOffset;
13154
13241
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, cursor.position)) : buffer.subarray(headerStart, cursor.position);
13155
- elements.set(
13156
- tag,
13242
+ defineElement(
13243
+ elements,
13157
13244
  new Element({
13158
13245
  tag,
13159
13246
  vr: "OB",
@@ -13164,7 +13251,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13164
13251
  byteOffset: headerStart,
13165
13252
  littleEndian: mode.littleEndian,
13166
13253
  items: result.items
13167
- })
13254
+ }),
13255
+ emit
13168
13256
  );
13169
13257
  continue;
13170
13258
  }
@@ -13183,8 +13271,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13183
13271
  cursor.position = cp246.endOffset;
13184
13272
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, cursor.position)) : buffer.subarray(headerStart, cursor.position);
13185
13273
  const privateCreator3 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13186
- elements.set(
13187
- tag,
13274
+ defineElement(
13275
+ elements,
13188
13276
  new Element({
13189
13277
  tag,
13190
13278
  vr: "SQ",
@@ -13197,15 +13285,16 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13197
13285
  littleEndian: mode.littleEndian,
13198
13286
  items: cp246.items,
13199
13287
  ...privateCreator3 !== void 0 ? { privateCreator: privateCreator3 } : {}
13200
- })
13288
+ }),
13289
+ emit
13201
13290
  );
13202
13291
  continue;
13203
13292
  }
13204
13293
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13205
13294
  const fallbackEnd = buffer.length;
13206
13295
  const fallbackBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, fallbackEnd)) : buffer.subarray(headerStart, fallbackEnd);
13207
- elements.set(
13208
- tag,
13296
+ defineElement(
13297
+ elements,
13209
13298
  new Element({
13210
13299
  tag,
13211
13300
  vr: "UN",
@@ -13215,7 +13304,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13215
13304
  byteOffset: headerStart,
13216
13305
  littleEndian: mode.littleEndian,
13217
13306
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
13218
- })
13307
+ }),
13308
+ emit
13219
13309
  );
13220
13310
  cursor.position = fallbackEnd;
13221
13311
  continue;
@@ -13250,8 +13340,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13250
13340
  applySpecificCharacterSet(tag, valueSlice, ctx, emit, position);
13251
13341
  const privateCreator = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13252
13342
  const finalVr = vr;
13253
- elements.set(
13254
- tag,
13343
+ defineElement(
13344
+ elements,
13255
13345
  new Element({
13256
13346
  tag,
13257
13347
  vr: finalVr,
@@ -13263,7 +13353,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13263
13353
  littleEndian: mode.littleEndian,
13264
13354
  ...ctx.currentCharset !== void 0 ? { specificCharacterSet: ctx.currentCharset } : {},
13265
13355
  ...privateCreator !== void 0 ? { privateCreator } : {}
13266
- })
13356
+ }),
13357
+ emit
13267
13358
  );
13268
13359
  }
13269
13360
  return { elements, endOffset: cursor.position };
@@ -15095,6 +15186,19 @@ function itemStreamOverrunsSequence(el, encoding) {
15095
15186
  const headerBytes = encoding === "implicit" ? 0 : EXPLICIT_SQ_HEADER_BYTES;
15096
15187
  return el.rawBytes.length - headerBytes > el.length;
15097
15188
  }
15189
+ function settledBound(source, encoding) {
15190
+ let index = source.length;
15191
+ let byteOffset;
15192
+ for (const [at, el] of source.entries()) {
15193
+ if (!itemStreamOverrunsSequence(el, encoding) && !isUnauditableSequence(el)) continue;
15194
+ if (index === source.length) index = at + 1;
15195
+ if (byteOffset === void 0 || el.byteOffset < byteOffset) byteOffset = el.byteOffset;
15196
+ }
15197
+ return { index, byteOffset };
15198
+ }
15199
+ function isSettled(el, at, bound) {
15200
+ return at < bound.index && (bound.byteOffset === void 0 || el.byteOffset <= bound.byteOffset);
15201
+ }
15098
15202
  function keepsPrivate(el, ctx, creators) {
15099
15203
  if (!ctx.active.has("RetainSafePrivate") || ctx.profile === void 0) return false;
15100
15204
  if (isPrivateCreatorElement(el.tag)) {
@@ -15114,10 +15218,11 @@ function processElements(source, ctx, contextPath, reservationsUsable) {
15114
15218
  undefinedVrElements: [],
15115
15219
  warnings: []
15116
15220
  };
15117
- const creators = creatorsInScope(source);
15118
- for (const el of source) {
15221
+ const bound = settledBound(source, ctx.encoding);
15222
+ const creators = creatorsInScope(source.filter((el, at) => isSettled(el, at, bound)));
15223
+ for (const [at, el] of source.entries()) {
15119
15224
  if (isPrivateTag(el.tag)) {
15120
- if (reservationsUsable && keepsPrivate(el, ctx, creators))
15225
+ if (reservationsUsable && isSettled(el, at, bound) && keepsPrivate(el, ctx, creators))
15121
15226
  keepOrEmpty(el, ctx, contextPath, out);
15122
15227
  else out.removedPrivateTags.push(el.tag);
15123
15228
  continue;
@@ -15250,6 +15355,47 @@ function defaultMethod(active) {
15250
15355
  const base = "Cosyte @cosyte/dicom: PS3.15 Basic Application Level Confidentiality Profile";
15251
15356
  return active.size === 0 ? base : `${base} + ${[...active].join(", ")}`;
15252
15357
  }
15358
+ var VALUE_DELIMITER = 92;
15359
+ var MAX_SHORT_FORM_VALUE_BYTES = 65534;
15360
+ function addDeidentificationMethod(existing, method) {
15361
+ const added = trimTrailingPad(buffer.Buffer.from(method, "latin1"));
15362
+ if (existing === void 0 || existing.vr !== "LO") {
15363
+ return { value: added, replacedPrior: false, retainedPrior: false };
15364
+ }
15365
+ const kept = buffer.Buffer.from(trimTrailingPad(existing.rawBytes));
15366
+ if (kept.length === 0) return { value: added, replacedPrior: false, retainedPrior: false };
15367
+ const keptValues = splitValues(kept).map(trimTrailingPad);
15368
+ const missing = added.length === 0 ? [] : splitValues(added).filter((v) => !keptValues.some((k) => k.equals(trimTrailingPad(v))));
15369
+ const parts = [kept];
15370
+ for (const value2 of missing) {
15371
+ parts.push(buffer.Buffer.from([VALUE_DELIMITER]), value2);
15372
+ }
15373
+ const value = trimTrailingPad(buffer.Buffer.concat(parts));
15374
+ if (value.length > MAX_SHORT_FORM_VALUE_BYTES) {
15375
+ return { value: added, replacedPrior: true, retainedPrior: false };
15376
+ }
15377
+ return { value, replacedPrior: false, retainedPrior: true };
15378
+ }
15379
+ function trimTrailingPad(value) {
15380
+ let end = value.length;
15381
+ while (end > 0) {
15382
+ const last = value[end - 1];
15383
+ if (last === 32 || last === 0) end--;
15384
+ else break;
15385
+ }
15386
+ return value.subarray(0, end);
15387
+ }
15388
+ function splitValues(value) {
15389
+ const out = [];
15390
+ let start = 0;
15391
+ for (let i = 0; i <= value.length; i++) {
15392
+ if (i === value.length || value[i] === VALUE_DELIMITER) {
15393
+ out.push(value.subarray(start, i));
15394
+ start = i + 1;
15395
+ }
15396
+ }
15397
+ return out;
15398
+ }
15253
15399
  function hasUncleanedBurnedIn(ds) {
15254
15400
  if (!ds.has(TAG_PIXEL_DATA)) return false;
15255
15401
  const flag = ds.get(TAG_BURNED_IN_ANNOTATION);
@@ -15283,11 +15429,23 @@ function deidentify(ds, options = {}) {
15283
15429
  insertedScalar(TAG_PATIENT_IDENTITY_REMOVED, "CS", buffer.Buffer.from("YES", "latin1"), littleEndian)
15284
15430
  );
15285
15431
  const method = options.deidentificationMethod ?? defaultMethod(active);
15432
+ const priorMethod = elements.get(TAG_DEIDENTIFICATION_METHOD);
15433
+ const deidentMethod = addDeidentificationMethod(priorMethod, method);
15286
15434
  elements.set(
15287
15435
  TAG_DEIDENTIFICATION_METHOD,
15288
- insertedScalar(TAG_DEIDENTIFICATION_METHOD, "LO", buffer.Buffer.from(method, "latin1"), littleEndian)
15436
+ insertedScalar(TAG_DEIDENTIFICATION_METHOD, "LO", deidentMethod.value, littleEndian)
15289
15437
  );
15290
15438
  const warnings = [...processed.warnings];
15439
+ if (deidentMethod.replacedPrior) {
15440
+ warnings.push(
15441
+ deidentMethodNotAdded({ byteOffset: priorMethod?.byteOffset ?? 0, fileMeta: false })
15442
+ );
15443
+ }
15444
+ if (deidentMethod.retainedPrior) {
15445
+ warnings.push(
15446
+ deidentMethodPriorRetained({ byteOffset: priorMethod?.byteOffset ?? 0, fileMeta: false })
15447
+ );
15448
+ }
15291
15449
  if (hasUncleanedBurnedIn(ds)) {
15292
15450
  const offset = ds.get(TAG_PIXEL_DATA)?.byteOffset ?? 0;
15293
15451
  warnings.push(burnedInAnnotationNotRemoved({ byteOffset: offset, fileMeta: false }));