@cosyte/dicom 0.0.10 → 0.0.12
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 +578 -7
- package/README.md +9 -3
- package/dist/index.cjs +212 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +107 -5
- package/dist/index.d.ts +107 -5
- package/dist/index.mjs +212 -35
- 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 has its own code, `DICOM_DEIDENT_METHOD_NOT_LO`. 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, and `report.warnings` now carries `DICOM_DEIDENT_METHOD_NOT_LO` when it is: the join concatenates `LO` values with the `5CH` delimiter, which is not defined over arbitrary octets, so replacing is deliberate and only the silence was the defect. **And the method text this library writes for itself is now multi-valued, one Value for the Profile and one per active Option.** PS3.5 2026c Table 6.2-1 caps an `LO` at **64 characters per Value** and `(0012,0063)` is `1-n`; the single-value string it wrote before measured **76** characters with no options, **130** with three and **272** with all nine, so every object de-identified without a caller-supplied method, in every release up to and including this one, carried a value no `LO` may legally hold - in the one attribute a strict receiver reads to decide whether the object was de-identified at all. **Re-de-identifying such an object keeps that 76-character Value**, because a prior record is added to rather than rewritten (measured: a flat 138 bytes over four passes, values of 76 and 61, `DICOM_DEIDENT_METHOD_PRIOR_RETAINED` raised for the retention and nothing said about the length). Your own `deidentificationMethod` is **not** bounded for you either: split it on `\` yourself if a strict receiver is in your path.
|
|
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.12";
|
|
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,12 @@ 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_NOT_LO: "DICOM_DEIDENT_METHOD_NOT_LO",
|
|
11752
|
+
// emitted by deidentify(), never by the parser
|
|
11753
|
+
DICOM_DEIDENT_METHOD_PRIOR_RETAINED: "DICOM_DEIDENT_METHOD_PRIOR_RETAINED",
|
|
11754
|
+
// emitted by deidentify(), never by the parser
|
|
11746
11755
|
DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE: "DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE",
|
|
11747
11756
|
// emitted by deidentify(), never by the parser
|
|
11748
11757
|
DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE: "DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE",
|
|
@@ -11767,6 +11776,41 @@ var WARNING_MESSAGES = Object.freeze({
|
|
|
11767
11776
|
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
11777
|
DICOM_UN_PARSED_AS_SQ: "Element ({tag}) has VR=UN with undefined length; descended as Implicit VR LE sequence per CP-246.",
|
|
11769
11778
|
DICOM_EMPTY_ITEM_IN_SEQUENCE: "Sequence ({tag}) contains an empty item (length=0); tolerated.",
|
|
11779
|
+
// Neither the tag nor the replaced element's value is here, and the tag's
|
|
11780
|
+
// absence is specific to this code rather than caution. See
|
|
11781
|
+
// `duplicateTagInDataSet`. Short by measurement rather than by intention: one
|
|
11782
|
+
// is raised per collision and the collision count is chosen by the input, so a
|
|
11783
|
+
// 400-character draft was measured at 50 M characters over a 1 MiB file and
|
|
11784
|
+
// cut. The reasoning lives in the factory's JSDoc, not in the string.
|
|
11785
|
+
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.",
|
|
11786
|
+
// The tag is deliberately absent for the same reason as the code above, and
|
|
11787
|
+
// the surviving copy is named because the File Meta group resolves a repeat
|
|
11788
|
+
// the OPPOSITE way round to a Data Set: first copy wins here, last read wins
|
|
11789
|
+
// there. Short for the same measured reason - one is raised per repeat and the
|
|
11790
|
+
// repeat count is chosen by the input.
|
|
11791
|
+
// 🛑 NO SPEC CITATION HERE, AND ITS ABSENCE IS DELIBERATE. A draft ended this
|
|
11792
|
+
// string "PS3.5 7.1.", which asserts that section's "at most once in a Data
|
|
11793
|
+
// Set" governs the File Meta group - the applicability this slice states it
|
|
11794
|
+
// cannot establish, because PS3.10 governs that group and is not vendored
|
|
11795
|
+
// here. Under `{ strict: true }` this string is also the thrown message, so an
|
|
11796
|
+
// unqualified citation there is the claim, not a footnote. Do not put it back.
|
|
11797
|
+
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.",
|
|
11798
|
+
// No value and no length: the prior value is the file's own text. The tag is a
|
|
11799
|
+
// constant of this code rather than composed from input, as in the two
|
|
11800
|
+
// (0002,0000) messages above.
|
|
11801
|
+
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.",
|
|
11802
|
+
// No value, no length AND NO VR. The VR is two bytes read from the file, and a
|
|
11803
|
+
// fabricated header makes those two bytes document content - the shape that
|
|
11804
|
+
// put "ITHS" into DICOM_NONZERO_RESERVED_BYTES. Naming the VR here would read
|
|
11805
|
+
// as harmless and is exactly the same defect.
|
|
11806
|
+
DICOM_DEIDENT_METHOD_NOT_LO: "The (0012,0063) value the source file carried is encoded under a Value Representation other than LO, which is not a De-identification Method this run can add its own text to (PS3.15 E.1.1), so the earlier value was replaced. The replaced text is not in the output. The text and the VR are both withheld from this message.",
|
|
11807
|
+
// No value and no length, for the same reason as the code above: the retained
|
|
11808
|
+
// text is the file's own. The tag is a constant of this code.
|
|
11809
|
+
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.",
|
|
11810
|
+
// The Item's own declared length is deliberately absent. See
|
|
11811
|
+
// `itemCrossesSequenceEnd`; `{n2}` stays because the emit site's
|
|
11812
|
+
// `endLimit < buffer.length` conjunct bounds it by the buffer.
|
|
11813
|
+
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
11814
|
DICOM_PIXEL_DATA_LENGTH_MISMATCH: "(7FE0,0010) PixelData declared length {n} does not match computed {n2} bytes.",
|
|
11771
11815
|
DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR: "Private element ({tag}) under Implicit VR LE has no VR override; falling back to UN.",
|
|
11772
11816
|
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 +11895,21 @@ function unParsedAsSQ(position, tag) {
|
|
|
11851
11895
|
function sqNotDescended(position, tag) {
|
|
11852
11896
|
return build(WARNING_CODES.DICOM_SQ_NOT_DESCENDED, position, { tag });
|
|
11853
11897
|
}
|
|
11898
|
+
function itemCrossesSequenceEnd(position, tag, availableLength) {
|
|
11899
|
+
return build(WARNING_CODES.DICOM_ITEM_CROSSES_SEQUENCE_END, position, {
|
|
11900
|
+
tag,
|
|
11901
|
+
n2: availableLength
|
|
11902
|
+
});
|
|
11903
|
+
}
|
|
11854
11904
|
function emptyItemInSequence(position, tag) {
|
|
11855
11905
|
return build(WARNING_CODES.DICOM_EMPTY_ITEM_IN_SEQUENCE, position, { tag });
|
|
11856
11906
|
}
|
|
11907
|
+
function duplicateTagInDataSet(position) {
|
|
11908
|
+
return build(WARNING_CODES.DICOM_DUPLICATE_TAG_IN_DATA_SET, position);
|
|
11909
|
+
}
|
|
11910
|
+
function duplicateFileMetaElement(position) {
|
|
11911
|
+
return build(WARNING_CODES.DICOM_DUPLICATE_FILE_META_ELEMENT, position);
|
|
11912
|
+
}
|
|
11857
11913
|
function implicitVRForPrivateTagWithoutVR(position, tag) {
|
|
11858
11914
|
return build(WARNING_CODES.DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR, position, { tag });
|
|
11859
11915
|
}
|
|
@@ -11863,6 +11919,15 @@ function privateCreatorUnknown(position, tag) {
|
|
|
11863
11919
|
function burnedInAnnotationNotRemoved(position) {
|
|
11864
11920
|
return build(WARNING_CODES.DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED, position);
|
|
11865
11921
|
}
|
|
11922
|
+
function deidentMethodNotAdded(position) {
|
|
11923
|
+
return build(WARNING_CODES.DICOM_DEIDENT_METHOD_NOT_ADDED, position);
|
|
11924
|
+
}
|
|
11925
|
+
function deidentMethodNotLo(position) {
|
|
11926
|
+
return build(WARNING_CODES.DICOM_DEIDENT_METHOD_NOT_LO, position);
|
|
11927
|
+
}
|
|
11928
|
+
function deidentMethodPriorRetained(position) {
|
|
11929
|
+
return build(WARNING_CODES.DICOM_DEIDENT_METHOD_PRIOR_RETAINED, position);
|
|
11930
|
+
}
|
|
11866
11931
|
function embeddedAttributeRemoved(position, tag, vr, count) {
|
|
11867
11932
|
return build(WARNING_CODES.DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED, position, {
|
|
11868
11933
|
tag,
|
|
@@ -12157,6 +12222,16 @@ function parseFileMeta(buffer$1, datasetStart, ctx, emit) {
|
|
|
12157
12222
|
fmElements.push(next);
|
|
12158
12223
|
}
|
|
12159
12224
|
}
|
|
12225
|
+
const seenModeledTags = /* @__PURE__ */ new Set();
|
|
12226
|
+
if (declaredFmLength !== void 0) seenModeledTags.add("00020000");
|
|
12227
|
+
for (const e of fmElements) {
|
|
12228
|
+
if (!MODELED_FM_TAGS.has(e.tag)) continue;
|
|
12229
|
+
if (seenModeledTags.has(e.tag)) {
|
|
12230
|
+
emit(duplicateFileMetaElement({ byteOffset: e.byteOffset, fileMeta: true }));
|
|
12231
|
+
continue;
|
|
12232
|
+
}
|
|
12233
|
+
seenModeledTags.add(e.tag);
|
|
12234
|
+
}
|
|
12160
12235
|
const tsElement = fmElements.find((e) => e.tag === "00020010");
|
|
12161
12236
|
if (tsElement === void 0 || tsElement.vr !== "UI") {
|
|
12162
12237
|
throw new DicomParseError(
|
|
@@ -12203,7 +12278,7 @@ function readExplicitLeElement(cursor) {
|
|
|
12203
12278
|
if (bytesConsumed !== headerLength + length) {
|
|
12204
12279
|
throw new RangeError("File Meta element accounting mismatch");
|
|
12205
12280
|
}
|
|
12206
|
-
return { tag, vr, value, bytesConsumed };
|
|
12281
|
+
return { tag, vr, value, bytesConsumed, byteOffset: headerStart };
|
|
12207
12282
|
}
|
|
12208
12283
|
function trimUI(buf) {
|
|
12209
12284
|
let end = buf.length;
|
|
@@ -12643,6 +12718,14 @@ var Element = class {
|
|
|
12643
12718
|
}
|
|
12644
12719
|
};
|
|
12645
12720
|
|
|
12721
|
+
// src/parser/data-set-map.ts
|
|
12722
|
+
function defineElement(elements, element, emit) {
|
|
12723
|
+
if (elements.has(element.tag)) {
|
|
12724
|
+
emit(duplicateTagInDataSet({ byteOffset: element.byteOffset }));
|
|
12725
|
+
}
|
|
12726
|
+
elements.set(element.tag, element);
|
|
12727
|
+
}
|
|
12728
|
+
|
|
12646
12729
|
// src/dataset/item.ts
|
|
12647
12730
|
var Item = class extends Dataset {
|
|
12648
12731
|
index;
|
|
@@ -12764,6 +12847,15 @@ function parseSequence(buffer, valueStart, ctx, emit, opts) {
|
|
|
12764
12847
|
items.push(new Item({ warnings: [], elements: inner.elements, index: itemIndex }));
|
|
12765
12848
|
cursor.position = inner.endOffset;
|
|
12766
12849
|
} else {
|
|
12850
|
+
if (opts.explicitLength !== void 0 && endLimit < buffer.length && cursor.position + itemLength > endLimit) {
|
|
12851
|
+
emit(
|
|
12852
|
+
itemCrossesSequenceEnd(
|
|
12853
|
+
{ byteOffset: itemHeaderStart },
|
|
12854
|
+
itemTag,
|
|
12855
|
+
Math.max(0, endLimit - cursor.position)
|
|
12856
|
+
)
|
|
12857
|
+
);
|
|
12858
|
+
}
|
|
12767
12859
|
if (cursor.position + itemLength > buffer.length) {
|
|
12768
12860
|
throw new DicomParseError(
|
|
12769
12861
|
FATAL_CODES.INVALID_FILE_META,
|
|
@@ -12899,8 +12991,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
|
|
|
12899
12991
|
cursor.position = seq.endOffset;
|
|
12900
12992
|
const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(valueRawStart, cursor.position)) : buffer.subarray(valueRawStart, cursor.position);
|
|
12901
12993
|
const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
|
|
12902
|
-
|
|
12903
|
-
|
|
12994
|
+
defineElement(
|
|
12995
|
+
elements,
|
|
12904
12996
|
new Element({
|
|
12905
12997
|
tag,
|
|
12906
12998
|
vr,
|
|
@@ -12911,7 +13003,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
|
|
|
12911
13003
|
littleEndian: true,
|
|
12912
13004
|
items: seq.items,
|
|
12913
13005
|
...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
|
|
12914
|
-
})
|
|
13006
|
+
}),
|
|
13007
|
+
emit
|
|
12915
13008
|
);
|
|
12916
13009
|
continue;
|
|
12917
13010
|
}
|
|
@@ -12930,8 +13023,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
|
|
|
12930
13023
|
cursor.position = cp246.endOffset;
|
|
12931
13024
|
const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(valueRawStart, cursor.position)) : buffer.subarray(valueRawStart, cursor.position);
|
|
12932
13025
|
const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
|
|
12933
|
-
|
|
12934
|
-
|
|
13026
|
+
defineElement(
|
|
13027
|
+
elements,
|
|
12935
13028
|
new Element({
|
|
12936
13029
|
tag,
|
|
12937
13030
|
vr: "SQ",
|
|
@@ -12944,7 +13037,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
|
|
|
12944
13037
|
littleEndian: true,
|
|
12945
13038
|
items: cp246.items,
|
|
12946
13039
|
...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
|
|
12947
|
-
})
|
|
13040
|
+
}),
|
|
13041
|
+
emit
|
|
12948
13042
|
);
|
|
12949
13043
|
continue;
|
|
12950
13044
|
}
|
|
@@ -12980,8 +13074,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
|
|
|
12980
13074
|
);
|
|
12981
13075
|
if (seq.success) {
|
|
12982
13076
|
const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
|
|
12983
|
-
|
|
12984
|
-
|
|
13077
|
+
defineElement(
|
|
13078
|
+
elements,
|
|
12985
13079
|
new Element({
|
|
12986
13080
|
tag,
|
|
12987
13081
|
vr,
|
|
@@ -12992,7 +13086,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
|
|
|
12992
13086
|
littleEndian: true,
|
|
12993
13087
|
items: seq.items,
|
|
12994
13088
|
...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
|
|
12995
|
-
})
|
|
13089
|
+
}),
|
|
13090
|
+
emit
|
|
12996
13091
|
);
|
|
12997
13092
|
continue;
|
|
12998
13093
|
}
|
|
@@ -13005,8 +13100,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
|
|
|
13005
13100
|
}
|
|
13006
13101
|
applySpecificCharacterSet(tag, valueSlice, ctx, emit, position);
|
|
13007
13102
|
const privateCreator = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
|
|
13008
|
-
|
|
13009
|
-
|
|
13103
|
+
defineElement(
|
|
13104
|
+
elements,
|
|
13010
13105
|
new Element({
|
|
13011
13106
|
tag,
|
|
13012
13107
|
vr,
|
|
@@ -13018,7 +13113,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
|
|
|
13018
13113
|
littleEndian: true,
|
|
13019
13114
|
...ctx.currentCharset !== void 0 ? { specificCharacterSet: ctx.currentCharset } : {},
|
|
13020
13115
|
...privateCreator !== void 0 ? { privateCreator } : {}
|
|
13021
|
-
})
|
|
13116
|
+
}),
|
|
13117
|
+
emit
|
|
13022
13118
|
);
|
|
13023
13119
|
}
|
|
13024
13120
|
return { elements, endOffset: cursor.position };
|
|
@@ -13125,8 +13221,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
|
|
|
13125
13221
|
cursor.position = seqResult.endOffset;
|
|
13126
13222
|
const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, cursor.position)) : buffer.subarray(headerStart, cursor.position);
|
|
13127
13223
|
const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
|
|
13128
|
-
|
|
13129
|
-
|
|
13224
|
+
defineElement(
|
|
13225
|
+
elements,
|
|
13130
13226
|
new Element({
|
|
13131
13227
|
tag,
|
|
13132
13228
|
vr: "SQ",
|
|
@@ -13137,7 +13233,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
|
|
|
13137
13233
|
littleEndian: mode.littleEndian,
|
|
13138
13234
|
items: seqResult.items,
|
|
13139
13235
|
...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
|
|
13140
|
-
})
|
|
13236
|
+
}),
|
|
13237
|
+
emit
|
|
13141
13238
|
);
|
|
13142
13239
|
continue;
|
|
13143
13240
|
}
|
|
@@ -13152,8 +13249,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
|
|
|
13152
13249
|
const result = parseSequence(buffer, valueStart2, ctx, emit, seqOpts);
|
|
13153
13250
|
cursor.position = result.endOffset;
|
|
13154
13251
|
const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, cursor.position)) : buffer.subarray(headerStart, cursor.position);
|
|
13155
|
-
|
|
13156
|
-
|
|
13252
|
+
defineElement(
|
|
13253
|
+
elements,
|
|
13157
13254
|
new Element({
|
|
13158
13255
|
tag,
|
|
13159
13256
|
vr: "OB",
|
|
@@ -13164,7 +13261,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
|
|
|
13164
13261
|
byteOffset: headerStart,
|
|
13165
13262
|
littleEndian: mode.littleEndian,
|
|
13166
13263
|
items: result.items
|
|
13167
|
-
})
|
|
13264
|
+
}),
|
|
13265
|
+
emit
|
|
13168
13266
|
);
|
|
13169
13267
|
continue;
|
|
13170
13268
|
}
|
|
@@ -13183,8 +13281,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
|
|
|
13183
13281
|
cursor.position = cp246.endOffset;
|
|
13184
13282
|
const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, cursor.position)) : buffer.subarray(headerStart, cursor.position);
|
|
13185
13283
|
const privateCreator3 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
|
|
13186
|
-
|
|
13187
|
-
|
|
13284
|
+
defineElement(
|
|
13285
|
+
elements,
|
|
13188
13286
|
new Element({
|
|
13189
13287
|
tag,
|
|
13190
13288
|
vr: "SQ",
|
|
@@ -13197,15 +13295,16 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
|
|
|
13197
13295
|
littleEndian: mode.littleEndian,
|
|
13198
13296
|
items: cp246.items,
|
|
13199
13297
|
...privateCreator3 !== void 0 ? { privateCreator: privateCreator3 } : {}
|
|
13200
|
-
})
|
|
13298
|
+
}),
|
|
13299
|
+
emit
|
|
13201
13300
|
);
|
|
13202
13301
|
continue;
|
|
13203
13302
|
}
|
|
13204
13303
|
const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
|
|
13205
13304
|
const fallbackEnd = buffer.length;
|
|
13206
13305
|
const fallbackBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, fallbackEnd)) : buffer.subarray(headerStart, fallbackEnd);
|
|
13207
|
-
|
|
13208
|
-
|
|
13306
|
+
defineElement(
|
|
13307
|
+
elements,
|
|
13209
13308
|
new Element({
|
|
13210
13309
|
tag,
|
|
13211
13310
|
vr: "UN",
|
|
@@ -13215,7 +13314,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
|
|
|
13215
13314
|
byteOffset: headerStart,
|
|
13216
13315
|
littleEndian: mode.littleEndian,
|
|
13217
13316
|
...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
|
|
13218
|
-
})
|
|
13317
|
+
}),
|
|
13318
|
+
emit
|
|
13219
13319
|
);
|
|
13220
13320
|
cursor.position = fallbackEnd;
|
|
13221
13321
|
continue;
|
|
@@ -13250,8 +13350,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
|
|
|
13250
13350
|
applySpecificCharacterSet(tag, valueSlice, ctx, emit, position);
|
|
13251
13351
|
const privateCreator = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
|
|
13252
13352
|
const finalVr = vr;
|
|
13253
|
-
|
|
13254
|
-
|
|
13353
|
+
defineElement(
|
|
13354
|
+
elements,
|
|
13255
13355
|
new Element({
|
|
13256
13356
|
tag,
|
|
13257
13357
|
vr: finalVr,
|
|
@@ -13263,7 +13363,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
|
|
|
13263
13363
|
littleEndian: mode.littleEndian,
|
|
13264
13364
|
...ctx.currentCharset !== void 0 ? { specificCharacterSet: ctx.currentCharset } : {},
|
|
13265
13365
|
...privateCreator !== void 0 ? { privateCreator } : {}
|
|
13266
|
-
})
|
|
13366
|
+
}),
|
|
13367
|
+
emit
|
|
13267
13368
|
);
|
|
13268
13369
|
}
|
|
13269
13370
|
return { elements, endOffset: cursor.position };
|
|
@@ -15095,6 +15196,19 @@ function itemStreamOverrunsSequence(el, encoding) {
|
|
|
15095
15196
|
const headerBytes = encoding === "implicit" ? 0 : EXPLICIT_SQ_HEADER_BYTES;
|
|
15096
15197
|
return el.rawBytes.length - headerBytes > el.length;
|
|
15097
15198
|
}
|
|
15199
|
+
function settledBound(source, encoding) {
|
|
15200
|
+
let index = source.length;
|
|
15201
|
+
let byteOffset;
|
|
15202
|
+
for (const [at, el] of source.entries()) {
|
|
15203
|
+
if (!itemStreamOverrunsSequence(el, encoding) && !isUnauditableSequence(el)) continue;
|
|
15204
|
+
if (index === source.length) index = at + 1;
|
|
15205
|
+
if (byteOffset === void 0 || el.byteOffset < byteOffset) byteOffset = el.byteOffset;
|
|
15206
|
+
}
|
|
15207
|
+
return { index, byteOffset };
|
|
15208
|
+
}
|
|
15209
|
+
function isSettled(el, at, bound) {
|
|
15210
|
+
return at < bound.index && (bound.byteOffset === void 0 || el.byteOffset <= bound.byteOffset);
|
|
15211
|
+
}
|
|
15098
15212
|
function keepsPrivate(el, ctx, creators) {
|
|
15099
15213
|
if (!ctx.active.has("RetainSafePrivate") || ctx.profile === void 0) return false;
|
|
15100
15214
|
if (isPrivateCreatorElement(el.tag)) {
|
|
@@ -15114,10 +15228,11 @@ function processElements(source, ctx, contextPath, reservationsUsable) {
|
|
|
15114
15228
|
undefinedVrElements: [],
|
|
15115
15229
|
warnings: []
|
|
15116
15230
|
};
|
|
15117
|
-
const
|
|
15118
|
-
|
|
15231
|
+
const bound = settledBound(source, ctx.encoding);
|
|
15232
|
+
const creators = creatorsInScope(source.filter((el, at) => isSettled(el, at, bound)));
|
|
15233
|
+
for (const [at, el] of source.entries()) {
|
|
15119
15234
|
if (isPrivateTag(el.tag)) {
|
|
15120
|
-
if (reservationsUsable && keepsPrivate(el, ctx, creators))
|
|
15235
|
+
if (reservationsUsable && isSettled(el, at, bound) && keepsPrivate(el, ctx, creators))
|
|
15121
15236
|
keepOrEmpty(el, ctx, contextPath, out);
|
|
15122
15237
|
else out.removedPrivateTags.push(el.tag);
|
|
15123
15238
|
continue;
|
|
@@ -15246,9 +15361,54 @@ function rebuildFileMeta(fileMeta, ctx) {
|
|
|
15246
15361
|
mediaStorageSOPInstanceUID: ctx.remap.map(fileMeta.mediaStorageSOPInstanceUID)
|
|
15247
15362
|
};
|
|
15248
15363
|
}
|
|
15364
|
+
var DEFAULT_METHOD_PROFILE = "@cosyte/dicom Basic Application Level Confidentiality Profile";
|
|
15249
15365
|
function defaultMethod(active) {
|
|
15250
|
-
const
|
|
15251
|
-
return
|
|
15366
|
+
const options = DEIDENTIFY_OPTIONS.filter((option) => active.has(option));
|
|
15367
|
+
return [DEFAULT_METHOD_PROFILE, ...options].join(String.fromCharCode(VALUE_DELIMITER));
|
|
15368
|
+
}
|
|
15369
|
+
var VALUE_DELIMITER = 92;
|
|
15370
|
+
var MAX_SHORT_FORM_VALUE_BYTES = 65534;
|
|
15371
|
+
function addDeidentificationMethod(existing, method) {
|
|
15372
|
+
const added = trimTrailingPad(buffer.Buffer.from(method, "latin1"));
|
|
15373
|
+
const none = { replacedPrior: false, retainedPrior: false, replacedNonLoPrior: false };
|
|
15374
|
+
if (existing === void 0) return { value: added, ...none };
|
|
15375
|
+
if (existing.vr !== "LO") {
|
|
15376
|
+
const lost = trimTrailingPad(existing.rawBytes).length > 0;
|
|
15377
|
+
return { value: added, ...none, replacedNonLoPrior: lost };
|
|
15378
|
+
}
|
|
15379
|
+
const kept = buffer.Buffer.from(trimTrailingPad(existing.rawBytes));
|
|
15380
|
+
if (kept.length === 0) return { value: added, ...none };
|
|
15381
|
+
const keptValues = splitValues(kept).map(trimTrailingPad);
|
|
15382
|
+
const missing = added.length === 0 ? [] : splitValues(added).filter((v) => !keptValues.some((k) => k.equals(trimTrailingPad(v))));
|
|
15383
|
+
const parts = [kept];
|
|
15384
|
+
for (const value2 of missing) {
|
|
15385
|
+
parts.push(buffer.Buffer.from([VALUE_DELIMITER]), value2);
|
|
15386
|
+
}
|
|
15387
|
+
const value = trimTrailingPad(buffer.Buffer.concat(parts));
|
|
15388
|
+
if (value.length > MAX_SHORT_FORM_VALUE_BYTES) {
|
|
15389
|
+
return { value: added, ...none, replacedPrior: true };
|
|
15390
|
+
}
|
|
15391
|
+
return { value, ...none, retainedPrior: true };
|
|
15392
|
+
}
|
|
15393
|
+
function trimTrailingPad(value) {
|
|
15394
|
+
let end = value.length;
|
|
15395
|
+
while (end > 0) {
|
|
15396
|
+
const last = value[end - 1];
|
|
15397
|
+
if (last === 32 || last === 0) end--;
|
|
15398
|
+
else break;
|
|
15399
|
+
}
|
|
15400
|
+
return value.subarray(0, end);
|
|
15401
|
+
}
|
|
15402
|
+
function splitValues(value) {
|
|
15403
|
+
const out = [];
|
|
15404
|
+
let start = 0;
|
|
15405
|
+
for (let i = 0; i <= value.length; i++) {
|
|
15406
|
+
if (i === value.length || value[i] === VALUE_DELIMITER) {
|
|
15407
|
+
out.push(value.subarray(start, i));
|
|
15408
|
+
start = i + 1;
|
|
15409
|
+
}
|
|
15410
|
+
}
|
|
15411
|
+
return out;
|
|
15252
15412
|
}
|
|
15253
15413
|
function hasUncleanedBurnedIn(ds) {
|
|
15254
15414
|
if (!ds.has(TAG_PIXEL_DATA)) return false;
|
|
@@ -15283,11 +15443,28 @@ function deidentify(ds, options = {}) {
|
|
|
15283
15443
|
insertedScalar(TAG_PATIENT_IDENTITY_REMOVED, "CS", buffer.Buffer.from("YES", "latin1"), littleEndian)
|
|
15284
15444
|
);
|
|
15285
15445
|
const method = options.deidentificationMethod ?? defaultMethod(active);
|
|
15446
|
+
const priorMethod = elements.get(TAG_DEIDENTIFICATION_METHOD);
|
|
15447
|
+
const deidentMethod = addDeidentificationMethod(priorMethod, method);
|
|
15286
15448
|
elements.set(
|
|
15287
15449
|
TAG_DEIDENTIFICATION_METHOD,
|
|
15288
|
-
insertedScalar(TAG_DEIDENTIFICATION_METHOD, "LO",
|
|
15450
|
+
insertedScalar(TAG_DEIDENTIFICATION_METHOD, "LO", deidentMethod.value, littleEndian)
|
|
15289
15451
|
);
|
|
15290
15452
|
const warnings = [...processed.warnings];
|
|
15453
|
+
if (deidentMethod.replacedPrior) {
|
|
15454
|
+
warnings.push(
|
|
15455
|
+
deidentMethodNotAdded({ byteOffset: priorMethod?.byteOffset ?? 0, fileMeta: false })
|
|
15456
|
+
);
|
|
15457
|
+
}
|
|
15458
|
+
if (deidentMethod.retainedPrior) {
|
|
15459
|
+
warnings.push(
|
|
15460
|
+
deidentMethodPriorRetained({ byteOffset: priorMethod?.byteOffset ?? 0, fileMeta: false })
|
|
15461
|
+
);
|
|
15462
|
+
}
|
|
15463
|
+
if (deidentMethod.replacedNonLoPrior) {
|
|
15464
|
+
warnings.push(
|
|
15465
|
+
deidentMethodNotLo({ byteOffset: priorMethod?.byteOffset ?? 0, fileMeta: false })
|
|
15466
|
+
);
|
|
15467
|
+
}
|
|
15291
15468
|
if (hasUncleanedBurnedIn(ds)) {
|
|
15292
15469
|
const offset = ds.get(TAG_PIXEL_DATA)?.byteOffset ?? 0;
|
|
15293
15470
|
warnings.push(burnedInAnnotationNotRemoved({ byteOffset: offset, fileMeta: false }));
|