@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/CHANGELOG.md +519 -7
- package/README.md +9 -3
- package/dist/index.cjs +191 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -5
- package/dist/index.d.ts +70 -5
- package/dist/index.mjs +191 -33
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
12903
|
-
|
|
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
|
-
|
|
12934
|
-
|
|
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
|
-
|
|
12984
|
-
|
|
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
|
-
|
|
13009
|
-
|
|
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
|
-
|
|
13129
|
-
|
|
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
|
-
|
|
13156
|
-
|
|
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
|
-
|
|
13187
|
-
|
|
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
|
-
|
|
13208
|
-
|
|
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
|
-
|
|
13254
|
-
|
|
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
|
|
15118
|
-
|
|
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",
|
|
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 }));
|