@cosyte/dicom 0.0.18 → 0.1.0
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 +874 -0
- package/README.md +329 -153
- package/dist/index.cjs +2758 -850
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1658 -269
- package/dist/index.d.ts +1658 -269
- package/dist/index.mjs +2755 -852
- package/dist/index.mjs.map +1 -1
- package/package.json +67 -35
package/dist/index.d.cts
CHANGED
|
@@ -15,11 +15,10 @@ import { Buffer } from 'node:buffer';
|
|
|
15
15
|
declare const VERSION: string;
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Type definitions for the
|
|
18
|
+
* Type definitions for the public Dictionary namespace.
|
|
19
19
|
*
|
|
20
|
-
* Frozen surface.
|
|
21
|
-
*
|
|
22
|
-
* shape - schema-breaking changes here cascade through the codebase.
|
|
20
|
+
* Frozen surface. The parser, the VR decoders and `validate()` all depend on
|
|
21
|
+
* this shape - schema-breaking changes here cascade through the codebase.
|
|
23
22
|
*
|
|
24
23
|
* @module
|
|
25
24
|
*/
|
|
@@ -98,19 +97,17 @@ interface UidEntry {
|
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
/**
|
|
101
|
-
* Public Dictionary namespace for `@cosyte/dicom
|
|
102
|
-
*
|
|
103
|
-
* D-10:
|
|
100
|
+
* Public Dictionary namespace for `@cosyte/dicom`.
|
|
104
101
|
*
|
|
105
102
|
* - {@link lookup} - accepts either an 8-char hex tag (`"00100010"`) or a
|
|
106
103
|
* keyword (`"PatientName"`) and returns the typed {@link DictionaryEntry}
|
|
107
104
|
* or `undefined`.
|
|
108
105
|
* - {@link byKeyword} - keyword-only lookup; for cases where the caller has
|
|
109
106
|
* already validated the input shape and wants a narrower call.
|
|
110
|
-
* - {@link uid} - UID lookup
|
|
107
|
+
* - {@link uid} - UID lookup.
|
|
111
108
|
*
|
|
112
|
-
* No function throws on miss
|
|
113
|
-
*
|
|
109
|
+
* No function throws on miss: each returns the typed entry or `undefined`.
|
|
110
|
+
* Returned entries are deeply frozen - mutation attempts
|
|
114
111
|
* throw `TypeError` in strict mode (which all of `@cosyte/dicom`'s emitted code
|
|
115
112
|
* runs under, given `"use strict"` is implicit for ES modules).
|
|
116
113
|
*
|
|
@@ -140,8 +137,8 @@ interface UidEntry {
|
|
|
140
137
|
* @example
|
|
141
138
|
* import { Dictionary } from "@cosyte/dicom";
|
|
142
139
|
* const a = Dictionary.lookup("00100010"); // by tag
|
|
143
|
-
* const b = Dictionary.lookup("PatientName"); // by keyword
|
|
144
|
-
* const c = Dictionary.lookup("not-real"); // undefined
|
|
140
|
+
* const b = Dictionary.lookup("PatientName"); // by keyword
|
|
141
|
+
* const c = Dictionary.lookup("not-real"); // undefined, never a throw
|
|
145
142
|
*/
|
|
146
143
|
declare function lookup(tagOrKeyword: string): DictionaryEntry | undefined;
|
|
147
144
|
/**
|
|
@@ -162,8 +159,8 @@ declare function byKeyword(keyword: string): DictionaryEntry | undefined;
|
|
|
162
159
|
* Covers PS3.6 Annex A whole: transfer syntaxes, SOP and Meta SOP Classes,
|
|
163
160
|
* well-known SOP Instances and Frames of Reference, coding schemes and the
|
|
164
161
|
* rest, current and retired alike. Returns `undefined` for unknown UIDs or
|
|
165
|
-
* malformed input. Used by `parseDicom()`
|
|
166
|
-
* Transfer Syntax names
|
|
162
|
+
* malformed input. Used by `parseDicom()` to render human-readable
|
|
163
|
+
* Transfer Syntax names.
|
|
167
164
|
*
|
|
168
165
|
* @example
|
|
169
166
|
* import { Dictionary } from "@cosyte/dicom";
|
|
@@ -187,10 +184,10 @@ declare namespace index {
|
|
|
187
184
|
/**
|
|
188
185
|
* Fatal error taxonomy for the `@cosyte/dicom` parser pipeline.
|
|
189
186
|
*
|
|
190
|
-
*
|
|
191
|
-
* -
|
|
192
|
-
*
|
|
193
|
-
* -
|
|
187
|
+
* Two rules hold the taxonomy in place:
|
|
188
|
+
* - `FATAL_CODES` is a frozen `as const` registry with EXACTLY four codes;
|
|
189
|
+
* anything less severe MUST be a Tier-2 warning.
|
|
190
|
+
* - `DicomParseError` carries `code`, `byteOffset`, `offsetFrame`,
|
|
194
191
|
* `snippet` (up to 16 source bytes, space-separated lowercase hex), and
|
|
195
192
|
* an optional `contextPath`. The thrown `Error.message` is formatted
|
|
196
193
|
* `[CODE] msg (offset=N frame=F)` with `… in path/segments` appended when
|
|
@@ -260,8 +257,7 @@ type FatalCode = (typeof FATAL_CODES)[keyof typeof FATAL_CODES];
|
|
|
260
257
|
* RESIDUALS` closed it, nothing on the thrown error said which. A consumer
|
|
261
258
|
* cutting `input.subarray(err.byteOffset, err.byteOffset + 16)` to see what
|
|
262
259
|
* upset the parser was, inside a Sequence Item, cutting an unrelated element -
|
|
263
|
-
* the exact defect the `{ strict: true }` snippet itself was fixed for
|
|
264
|
-
* `#80`.
|
|
260
|
+
* the exact defect the `{ strict: true }` snippet itself was once fixed for.
|
|
265
261
|
*
|
|
266
262
|
* **A frame NAME is published; a frame ORIGIN is not, and that asymmetry is
|
|
267
263
|
* deliberate.** The name is drawn from the closed set below, which the parser
|
|
@@ -331,7 +327,7 @@ type OffsetFrame = (typeof OFFSET_FRAMES)[keyof typeof OFFSET_FRAMES];
|
|
|
331
327
|
* Thrown by `parseDicom` when the input violates one of the four
|
|
332
328
|
* unrecoverable Tier-3 structural rules - or, under `{ strict: true }`,
|
|
333
329
|
* when any Tier-2 warning is escalated through the single `emit`
|
|
334
|
-
* chokepoint
|
|
330
|
+
* chokepoint. Carries byte-offset positional context plus a short
|
|
335
331
|
* source snippet so consumers can log actionable errors.
|
|
336
332
|
*
|
|
337
333
|
* Message format: `[CODE] msg (offset=N frame=F)`, with `… in a/b/c`
|
|
@@ -353,13 +349,14 @@ type OffsetFrame = (typeof OFFSET_FRAMES)[keyof typeof OFFSET_FRAMES];
|
|
|
353
349
|
* **`snippet` is cut in the frame `offsetFrame` names, on every fatal but one.**
|
|
354
350
|
* The exception is `UNSUPPORTED_TRANSFER_SYNTAX`, whose snippet slot carries
|
|
355
351
|
* PS3.6's own NAME for the unsupported UID when the registry publishes one
|
|
356
|
-
* (`"
|
|
352
|
+
* (`"SMPTE ST 2110-20 Uncompressed Progressive Active Video"`), and 16 raw
|
|
353
|
+
* bytes only when it does not. That is
|
|
357
354
|
* deliberate and predates the frame; it is named here because a universal about
|
|
358
|
-
* `snippet` written without it is false on the code
|
|
359
|
-
* first. Everywhere else the two agree, so a consumer
|
|
360
|
-
* at the offset already has them. **The frame is what
|
|
361
|
-
* indexing anything of its OWN by `byteOffset`**, which
|
|
362
|
-
* this class used to cover.
|
|
355
|
+
* `snippet` written without it is false on the code an object under a refused
|
|
356
|
+
* Transfer Syntax reaches first. Everywhere else the two agree, so a consumer
|
|
357
|
+
* that only wants the bytes at the offset already has them. **The frame is what
|
|
358
|
+
* a consumer needs before indexing anything of its OWN by `byteOffset`**, which
|
|
359
|
+
* is the case no field on this class used to cover.
|
|
363
360
|
*
|
|
364
361
|
* @example
|
|
365
362
|
* ```ts
|
|
@@ -388,7 +385,7 @@ declare class DicomParseError extends Error {
|
|
|
388
385
|
readonly contextPath: readonly string[] | undefined;
|
|
389
386
|
/**
|
|
390
387
|
* Construct a new `DicomParseError`. All fields except `contextPath` are
|
|
391
|
-
* required so every thrower populates positional context
|
|
388
|
+
* required so every thrower populates positional context - and
|
|
392
389
|
* `offsetFrame` is required for the same reason `byteOffset` is, because an
|
|
393
390
|
* offset whose frame is optional is an offset whose frame is usually
|
|
394
391
|
* missing.
|
|
@@ -401,11 +398,14 @@ declare class DicomParseError extends Error {
|
|
|
401
398
|
/**
|
|
402
399
|
* Shared parser-pipeline types for `@cosyte/dicom`.
|
|
403
400
|
*
|
|
404
|
-
*
|
|
405
|
-
* -
|
|
406
|
-
* -
|
|
407
|
-
*
|
|
408
|
-
* -
|
|
401
|
+
* The contracts that live here:
|
|
402
|
+
* - `ParseOptions`, the shape a caller passes to `parseDicom`.
|
|
403
|
+
* - `OnWarningCallback`, whose ordering contract is that it is invoked
|
|
404
|
+
* AFTER the push to `ctx.warnings`.
|
|
405
|
+
* - `DicomPosition` (`byteOffset`, optional `fileMeta` / `deflated` /
|
|
406
|
+
* `contextPath`).
|
|
407
|
+
* - `ParseContext`, the parser's internal state, including the active
|
|
408
|
+
* source/vendor `Profile`.
|
|
409
409
|
*
|
|
410
410
|
* Public types (exported via `src/index.ts`): `DicomPosition`, `ParseOptions`, `OnWarningCallback`.
|
|
411
411
|
* Internal type (NOT exported from `src/index.ts`): `ParseContext`.
|
|
@@ -435,7 +435,7 @@ interface PrivateTagDefinition {
|
|
|
435
435
|
readonly name: string;
|
|
436
436
|
}
|
|
437
437
|
/**
|
|
438
|
-
* A source/vendor tolerance preset
|
|
438
|
+
* A source/vendor tolerance preset. A `Profile` bundles three
|
|
439
439
|
* things that only ever **tighten or annotate** a parse - never loosen it
|
|
440
440
|
* past the Postel's-Law default:
|
|
441
441
|
*
|
|
@@ -478,7 +478,7 @@ interface Profile {
|
|
|
478
478
|
* Positional context for a `DicomParseWarning` or `DicomParseError`.
|
|
479
479
|
*
|
|
480
480
|
* **🛑 `byteOffset` IS NOT ALWAYS RELATIVE TO THE SOURCE BUFFER, AND THIS
|
|
481
|
-
* JSDOC SAID IT WAS.** For the Deflated Explicit VR LE transfer syntax
|
|
481
|
+
* JSDOC SAID IT WAS.** For the Deflated Explicit VR LE transfer syntax,
|
|
482
482
|
* `deflated: true` says the offset indexes the inflated dataset buffer rather
|
|
483
483
|
* than the on-disk source. That flag is the only frame this type carries, and
|
|
484
484
|
* it is not the only frame this parser has: a defined-length Sequence Item is
|
|
@@ -514,7 +514,7 @@ interface DicomPosition {
|
|
|
514
514
|
/**
|
|
515
515
|
* Synchronous callback invoked once per Tier-2 warning emitted during parse.
|
|
516
516
|
*
|
|
517
|
-
*
|
|
517
|
+
* The callback fires AFTER the warning has been
|
|
518
518
|
* pushed to `ctx.warnings`; if the callback throws, the parser silently
|
|
519
519
|
* swallows the exception and continues (mirrors `@cosyte/hl7` sibling).
|
|
520
520
|
*
|
|
@@ -532,8 +532,7 @@ type OnWarningCallback = (warning: DicomParseWarning) => void;
|
|
|
532
532
|
/**
|
|
533
533
|
* Options accepted by `parseDicom`.
|
|
534
534
|
*
|
|
535
|
-
*
|
|
536
|
-
* adds it. With `exactOptionalPropertyTypes: true`, callers omit unset keys
|
|
535
|
+
* With `exactOptionalPropertyTypes: true`, callers omit unset keys
|
|
537
536
|
* rather than passing `undefined` for any field below.
|
|
538
537
|
*
|
|
539
538
|
* @example
|
|
@@ -579,7 +578,7 @@ interface ParseOptions {
|
|
|
579
578
|
* treatment is in the package's troubleshooting docs. The `DicomParseError`
|
|
580
579
|
* this option raises in its place is a different and larger surface: it also
|
|
581
580
|
* carries `snippet`,
|
|
582
|
-
* **16 raw bytes, unredacted
|
|
581
|
+
* **16 raw bytes, unredacted**, read at the warning's own `byteOffset`.
|
|
583
582
|
* A message-only PHI review of the lenient path therefore does not transfer to
|
|
584
583
|
* the strict one. Log `err.code`, `err.byteOffset`, `err.offsetFrame` and
|
|
585
584
|
* `err.message`; treat `err.snippet` as PHI.
|
|
@@ -623,7 +622,7 @@ interface ParseOptions {
|
|
|
623
622
|
/**
|
|
624
623
|
* Synchronous callback invoked once per Tier-2 warning, after the warning
|
|
625
624
|
* has been pushed to `Dataset.warnings`. Throwing handlers are silently
|
|
626
|
-
* swallowed
|
|
625
|
+
* swallowed, for parser-state safety.
|
|
627
626
|
*
|
|
628
627
|
* Omit to skip the callback entirely.
|
|
629
628
|
*/
|
|
@@ -634,11 +633,11 @@ interface ParseOptions {
|
|
|
634
633
|
* default), `Element.rawBytes` is `Buffer.subarray(slice)` - a zero-copy
|
|
635
634
|
* view that pins the source ArrayBuffer until every Element is GC'd.
|
|
636
635
|
*
|
|
637
|
-
*
|
|
636
|
+
* Omit to use the default.
|
|
638
637
|
*/
|
|
639
638
|
readonly copyValues?: boolean;
|
|
640
639
|
/**
|
|
641
|
-
* Source/vendor tolerance preset
|
|
640
|
+
* Source/vendor tolerance preset. Applies the profile's
|
|
642
641
|
* `escalations` / `suppressions` to warning emission and its
|
|
643
642
|
* `privateDictionary` to Implicit-VR resolution of private data elements.
|
|
644
643
|
* A profile only tightens or annotates - it never makes the default
|
|
@@ -655,13 +654,12 @@ interface ParseOptions {
|
|
|
655
654
|
* Tier-2 warning registry and factories for the `@cosyte/dicom` parser
|
|
656
655
|
* pipeline.
|
|
657
656
|
*
|
|
658
|
-
*
|
|
659
|
-
* -
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
* factory carries its own JSDoc + `@example` and returns a typed
|
|
657
|
+
* Two rules govern it:
|
|
658
|
+
* - `WARNING_CODES` is a frozen `as const` registry carrying every code
|
|
659
|
+
* this package defines, including the ones no stage emits yet, so the
|
|
660
|
+
* schema is stable for consumers.
|
|
661
|
+
* - Exactly one named factory per actively-emitted code; each factory
|
|
662
|
+
* carries its own JSDoc + `@example` and returns a typed
|
|
665
663
|
* `DicomParseWarning`.
|
|
666
664
|
*
|
|
667
665
|
* Consumers compare `warning.code === WARNING_CODES.<CODE>` to narrow and
|
|
@@ -677,9 +675,8 @@ interface ParseOptions {
|
|
|
677
675
|
* The registry is frozen via `as const` so TypeScript infers the exact
|
|
678
676
|
* string-literal union for `WarningCode` - there is zero runtime cost and
|
|
679
677
|
* no magic-string comparisons for consumers. Reserved-but-not-emitted
|
|
680
|
-
* codes carry inline comments documenting which
|
|
681
|
-
*
|
|
682
|
-
* phases per D-08, D-42, D-43).
|
|
678
|
+
* codes carry inline comments documenting which stage activates them; the
|
|
679
|
+
* union is declared whole so the schema is stable for consumers.
|
|
683
680
|
*
|
|
684
681
|
* @example
|
|
685
682
|
* ```ts
|
|
@@ -691,6 +688,10 @@ interface ParseOptions {
|
|
|
691
688
|
* ```
|
|
692
689
|
*/
|
|
693
690
|
declare const WARNING_CODES: {
|
|
691
|
+
readonly DICOM_DIRECTORY_OFFSET_DEFLATED: "DICOM_DIRECTORY_OFFSET_DEFLATED";
|
|
692
|
+
readonly DICOM_DIRECTORY_OFFSET_MALFORMED: "DICOM_DIRECTORY_OFFSET_MALFORMED";
|
|
693
|
+
readonly DICOM_DIRECTORY_OFFSET_UNRESOLVED: "DICOM_DIRECTORY_OFFSET_UNRESOLVED";
|
|
694
|
+
readonly DICOM_DIRECTORY_RECORD_REVISITED: "DICOM_DIRECTORY_RECORD_REVISITED";
|
|
694
695
|
readonly DICOM_DUPLICATE_FILE_META_ELEMENT: "DICOM_DUPLICATE_FILE_META_ELEMENT";
|
|
695
696
|
readonly DICOM_DUPLICATE_TAG_IN_DATA_SET: "DICOM_DUPLICATE_TAG_IN_DATA_SET";
|
|
696
697
|
readonly DICOM_EMPTY_ITEM_IN_SEQUENCE: "DICOM_EMPTY_ITEM_IN_SEQUENCE";
|
|
@@ -702,6 +703,7 @@ declare const WARNING_CODES: {
|
|
|
702
703
|
readonly DICOM_MISSING_PREAMBLE: "DICOM_MISSING_PREAMBLE";
|
|
703
704
|
readonly DICOM_NONZERO_RESERVED_BYTES: "DICOM_NONZERO_RESERVED_BYTES";
|
|
704
705
|
readonly DICOM_ODD_LENGTH_VALUE_PADDED: "DICOM_ODD_LENGTH_VALUE_PADDED";
|
|
706
|
+
readonly DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED: "DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED";
|
|
705
707
|
readonly DICOM_PIXEL_DATA_LENGTH_MISMATCH: "DICOM_PIXEL_DATA_LENGTH_MISMATCH";
|
|
706
708
|
readonly DICOM_PRIVATE_TAG_NO_CREATOR: "DICOM_PRIVATE_TAG_NO_CREATOR";
|
|
707
709
|
readonly DICOM_SQ_NOT_DESCENDED: "DICOM_SQ_NOT_DESCENDED";
|
|
@@ -716,15 +718,27 @@ declare const WARNING_CODES: {
|
|
|
716
718
|
readonly DICOM_TRAILING_NULL_IN_TEXT_VR: "DICOM_TRAILING_NULL_IN_TEXT_VR";
|
|
717
719
|
readonly DICOM_UI_TRAILING_SPACE: "DICOM_UI_TRAILING_SPACE";
|
|
718
720
|
readonly DICOM_CHARSET_AMBIGUOUS_SEPARATOR: "DICOM_CHARSET_AMBIGUOUS_SEPARATOR";
|
|
721
|
+
readonly DICOM_CHARSET_BYTES_UNDECODABLE: "DICOM_CHARSET_BYTES_UNDECODABLE";
|
|
722
|
+
readonly DICOM_CHARSET_ESCAPE_UNDECLARED: "DICOM_CHARSET_ESCAPE_UNDECLARED";
|
|
723
|
+
readonly DICOM_CHARSET_EXTENSION_NOT_RESET: "DICOM_CHARSET_EXTENSION_NOT_RESET";
|
|
719
724
|
readonly DICOM_UNSUPPORTED_CHARSET: "DICOM_UNSUPPORTED_CHARSET";
|
|
720
725
|
readonly DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED: "DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED";
|
|
726
|
+
readonly DICOM_DEIDENT_DATES_NOT_TRANSFORMED: "DICOM_DEIDENT_DATES_NOT_TRANSFORMED";
|
|
727
|
+
readonly DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED: "DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED";
|
|
721
728
|
readonly DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED: "DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED";
|
|
729
|
+
readonly DICOM_DEIDENT_FILE_META_REPLACED: "DICOM_DEIDENT_FILE_META_REPLACED";
|
|
730
|
+
readonly DICOM_DEIDENT_GROUP_0004_REMOVED: "DICOM_DEIDENT_GROUP_0004_REMOVED";
|
|
731
|
+
readonly DICOM_DEIDENT_METHOD_CODES_PRIOR_REPLACED: "DICOM_DEIDENT_METHOD_CODES_PRIOR_REPLACED";
|
|
732
|
+
readonly DICOM_DEIDENT_METHOD_CODES_PRIOR_RETAINED: "DICOM_DEIDENT_METHOD_CODES_PRIOR_RETAINED";
|
|
722
733
|
readonly DICOM_DEIDENT_METHOD_NOT_ADDED: "DICOM_DEIDENT_METHOD_NOT_ADDED";
|
|
723
734
|
readonly DICOM_DEIDENT_METHOD_NOT_LO: "DICOM_DEIDENT_METHOD_NOT_LO";
|
|
724
735
|
readonly DICOM_DEIDENT_METHOD_PRIOR_RETAINED: "DICOM_DEIDENT_METHOD_PRIOR_RETAINED";
|
|
725
736
|
readonly DICOM_DEIDENT_METHOD_VALUE_OVER_LENGTH: "DICOM_DEIDENT_METHOD_VALUE_OVER_LENGTH";
|
|
737
|
+
readonly DICOM_DEIDENT_PRIVATE_CARRIER_NOT_AUDITABLE: "DICOM_DEIDENT_PRIVATE_CARRIER_NOT_AUDITABLE";
|
|
738
|
+
readonly DICOM_DEIDENT_PRIVATE_DECLARATION_NOT_RESOLVED: "DICOM_DEIDENT_PRIVATE_DECLARATION_NOT_RESOLVED";
|
|
726
739
|
readonly DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE: "DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE";
|
|
727
740
|
readonly DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE: "DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE";
|
|
741
|
+
readonly DICOM_DEIDENT_UNREGISTERED_ELEMENT_REMOVED: "DICOM_DEIDENT_UNREGISTERED_ELEMENT_REMOVED";
|
|
728
742
|
readonly DICOM_PRIVATE_CREATOR_UNKNOWN: "DICOM_PRIVATE_CREATOR_UNKNOWN";
|
|
729
743
|
};
|
|
730
744
|
/**
|
|
@@ -750,7 +764,7 @@ type WarningCode = (typeof WARNING_CODES)[keyof typeof WARNING_CODES];
|
|
|
750
764
|
* subclass) so they can be safely accumulated into `Dataset.warnings` and
|
|
751
765
|
* passed to `onWarning` callbacks.
|
|
752
766
|
*
|
|
753
|
-
*
|
|
767
|
+
* There is intentionally NO `snippet` field on warnings:
|
|
754
768
|
* real-world files routinely produce 50+ warnings and a per-warning
|
|
755
769
|
* snippet would balloon retained memory. Snippets appear only on
|
|
756
770
|
* `DicomParseError` (the strict-mode escalation path).
|
|
@@ -772,43 +786,66 @@ interface DicomParseWarning {
|
|
|
772
786
|
}
|
|
773
787
|
|
|
774
788
|
/**
|
|
775
|
-
*
|
|
789
|
+
* The structural `Item` - a single `(FFFE,E000)`-delimited Item
|
|
776
790
|
* inside a Sequence, carrying a nested `Dataset`.
|
|
777
791
|
*
|
|
778
|
-
*
|
|
779
|
-
* land in Phase 3 (via `Dataset` extension per D-42).
|
|
792
|
+
* Structural surface only; navigation comes from the `Dataset` superclass.
|
|
780
793
|
*
|
|
781
794
|
* @module
|
|
782
795
|
*/
|
|
783
796
|
|
|
784
797
|
/**
|
|
785
798
|
* Initialiser shape for an `Item` - extends `DatasetInit` with `index`,
|
|
786
|
-
* the 0-based position of the item inside its parent `Sequence
|
|
799
|
+
* the 0-based position of the item inside its parent `Sequence`, and the
|
|
800
|
+
* optional `fileOffset` the parser records (see {@link Item.fileOffset}).
|
|
787
801
|
*
|
|
788
802
|
* @internal
|
|
789
803
|
*/
|
|
790
804
|
interface ItemInit extends DatasetInit {
|
|
791
805
|
readonly index: number;
|
|
806
|
+
readonly fileOffset?: number;
|
|
792
807
|
}
|
|
793
808
|
/**
|
|
794
809
|
* One sequence item. Inherits `fileMeta` (always `undefined` for nested
|
|
795
810
|
* items), `warnings`, and the protected element map from `Dataset`.
|
|
796
811
|
*
|
|
797
|
-
*
|
|
798
|
-
*
|
|
812
|
+
* `Item.get(...)` / `Item.has(...)` and the rest come from the `Dataset`
|
|
813
|
+
* superclass.
|
|
799
814
|
*
|
|
800
815
|
* @example
|
|
801
816
|
* ```ts
|
|
802
817
|
* import { Item } from "@cosyte/dicom";
|
|
803
|
-
* //
|
|
818
|
+
* // The parser constructs items as follows:
|
|
804
819
|
* // const item = new Item({ index: 0, warnings: [], elements: new Map() });
|
|
805
820
|
* ```
|
|
806
821
|
*/
|
|
807
822
|
declare class Item extends Dataset {
|
|
808
823
|
readonly index: number;
|
|
824
|
+
/**
|
|
825
|
+
* Byte offset of this Item's `(FFFE,E000)` Item tag in the Part 10 file it
|
|
826
|
+
* was read from, counted from the first byte of the File Preamble, which is
|
|
827
|
+
* how PS3.3 2026d Table F.3-3 counts a DICOMDIR's offsets. For a file read
|
|
828
|
+
* without a preamble, the 132 bytes of File Preamble and `DICM` prefix it
|
|
829
|
+
* lacks are still counted, so the File Meta group's first byte is offset 132.
|
|
830
|
+
*
|
|
831
|
+
* `undefined` for an Item with no position in a file: one inside a Deflated
|
|
832
|
+
* Data Set (a byte of a deflated stream is not a byte of the file), and one
|
|
833
|
+
* built by hand. `deidentify()` carries it from each source Item to the Item
|
|
834
|
+
* it rebuilds, as that Item's identity: it is how `serializeDicom` ties a
|
|
835
|
+
* DICOMDIR offset to the Directory Record it named.
|
|
836
|
+
*
|
|
837
|
+
* @example
|
|
838
|
+
* ```ts
|
|
839
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
840
|
+
* const ds = parseDicom(buf);
|
|
841
|
+
* const first = ds.get("00041220")?.items?.[0];
|
|
842
|
+
* first?.fileOffset; // where that Directory Record Item starts in the file
|
|
843
|
+
* ```
|
|
844
|
+
*/
|
|
845
|
+
readonly fileOffset: number | undefined;
|
|
809
846
|
/**
|
|
810
847
|
* Construct a new structural `Item`. Producers are the SQ / FFFE
|
|
811
|
-
* marker parsers
|
|
848
|
+
* marker parsers.
|
|
812
849
|
*
|
|
813
850
|
* @internal
|
|
814
851
|
*/
|
|
@@ -816,7 +853,7 @@ declare class Item extends Dataset {
|
|
|
816
853
|
}
|
|
817
854
|
|
|
818
855
|
/**
|
|
819
|
-
* Decoded-value model
|
|
856
|
+
* Decoded-value model - the typed result of `Element.value`.
|
|
820
857
|
*
|
|
821
858
|
* `DicomValue` is a discriminated union keyed on `kind`; each variant maps
|
|
822
859
|
* to a family of VRs that share a decode shape (per PS3.5 §6.2 Table 6.2-1):
|
|
@@ -1021,18 +1058,14 @@ type DicomValue = {
|
|
|
1021
1058
|
};
|
|
1022
1059
|
|
|
1023
1060
|
/**
|
|
1024
|
-
*
|
|
1061
|
+
* The structural `Element` - leaf wrapper over an on-wire DICOM
|
|
1025
1062
|
* Data Element.
|
|
1026
1063
|
*
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
* buffer by default (zero-copy), pinning the source ArrayBuffer until
|
|
1033
|
-
* every Element is GC'd. The `parseDicom` `{ copyValues: true }` option
|
|
1034
|
-
* flips production to `Buffer.from(slice)` so the source can be
|
|
1035
|
-
* released; producers (parser plans 02-04 onward) honour it.
|
|
1064
|
+
* `rawBytes` is a `Buffer.subarray()` view over the source buffer by default
|
|
1065
|
+
* (zero-copy), pinning the source ArrayBuffer until every Element is GC'd.
|
|
1066
|
+
* The `parseDicom` `{ copyValues: true }` option flips production to
|
|
1067
|
+
* `Buffer.from(slice)` so the source can be released; every producer in the
|
|
1068
|
+
* parser honours it.
|
|
1036
1069
|
*
|
|
1037
1070
|
* @module
|
|
1038
1071
|
*/
|
|
@@ -1042,7 +1075,7 @@ type DicomValue = {
|
|
|
1042
1075
|
*
|
|
1043
1076
|
* `privateCreator` is omitted (not `undefined`) for standard tags;
|
|
1044
1077
|
* present only when the parser resolved a `(gggg,EEFF)` private element
|
|
1045
|
-
* to its registered Private Creator string
|
|
1078
|
+
* to its registered Private Creator string.
|
|
1046
1079
|
*
|
|
1047
1080
|
* @internal
|
|
1048
1081
|
*/
|
|
@@ -1055,10 +1088,10 @@ interface ElementInit {
|
|
|
1055
1088
|
readonly byteOffset: number;
|
|
1056
1089
|
readonly privateCreator?: string;
|
|
1057
1090
|
/**
|
|
1058
|
-
*
|
|
1059
|
-
* `VR=UN` with undefined length to `VR=SQ` via the CP-246 fallback
|
|
1060
|
-
*
|
|
1061
|
-
* first
|
|
1091
|
+
* Hint set by the parser when an `Element` was promoted from
|
|
1092
|
+
* `VR=UN` with undefined length to `VR=SQ` via the CP-246 fallback.
|
|
1093
|
+
* The lazy value decoder reads this to choose the inner SQ decoder
|
|
1094
|
+
* (Implicit VR LE) on first access. Never set on standard SQ elements.
|
|
1062
1095
|
*
|
|
1063
1096
|
* @internal
|
|
1064
1097
|
*/
|
|
@@ -1066,14 +1099,14 @@ interface ElementInit {
|
|
|
1066
1099
|
/**
|
|
1067
1100
|
* Byte order of the value bytes, set by the parser per transfer syntax
|
|
1068
1101
|
* (`true` for Implicit/Explicit VR LE + Deflated; `false` for Explicit VR
|
|
1069
|
-
* BE).
|
|
1102
|
+
* BE). The numeric decoders read this - signedness comes from the VR,
|
|
1070
1103
|
* endianness from here.
|
|
1071
1104
|
*/
|
|
1072
1105
|
readonly littleEndian: boolean;
|
|
1073
1106
|
/**
|
|
1074
1107
|
* The dataset's resolved `(0008,0005)` Specific Character Set terms in
|
|
1075
1108
|
* effect for this element (parent dataset's, or the item's own override).
|
|
1076
|
-
* Omitted when the Default Repertoire (ISO_IR 6) applies.
|
|
1109
|
+
* Omitted when the Default Repertoire (ISO_IR 6) applies. The
|
|
1077
1110
|
* charset-dependent text decoders (`LO SH UC LT ST UT PN`) read this.
|
|
1078
1111
|
*/
|
|
1079
1112
|
readonly specificCharacterSet?: readonly string[];
|
|
@@ -1085,13 +1118,11 @@ interface ElementInit {
|
|
|
1085
1118
|
readonly items?: readonly Item[];
|
|
1086
1119
|
}
|
|
1087
1120
|
/**
|
|
1088
|
-
* One DICOM Data Element as parsed
|
|
1121
|
+
* One DICOM Data Element as parsed.
|
|
1089
1122
|
*
|
|
1090
|
-
*
|
|
1091
|
-
* `byteOffset`, `privateCreator`.
|
|
1092
|
-
*
|
|
1093
|
-
* memoized `.value` getter and the VR-aware decoders under
|
|
1094
|
-
* `src/dataset/vr/`.
|
|
1123
|
+
* Structural surface: `tag`, `vr`, `vm`, `length`, `rawBytes`,
|
|
1124
|
+
* `byteOffset`, `privateCreator`. On top of it sits a lazy, memoized
|
|
1125
|
+
* `.value` getter backed by the VR-aware decoders under `src/dataset/vr/`.
|
|
1095
1126
|
*
|
|
1096
1127
|
* `rawBytes` retention behaviour: zero-copy `Buffer.subarray` view by
|
|
1097
1128
|
* default (pins source ArrayBuffer); pass `{ copyValues: true }` to
|
|
@@ -1124,10 +1155,10 @@ declare class Element {
|
|
|
1124
1155
|
readonly byteOffset: number;
|
|
1125
1156
|
readonly privateCreator: string | undefined;
|
|
1126
1157
|
/**
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1158
|
+
* Hint for the lazy SQ decoder. `true` when this Element was promoted
|
|
1159
|
+
* from `VR=UN` with undefined length to `VR=SQ` via the CP-246 fallback,
|
|
1160
|
+
* which is what selects the Implicit VR LE inner decoder. Always
|
|
1161
|
+
* `undefined` on standard SQ elements.
|
|
1131
1162
|
*
|
|
1132
1163
|
* @internal
|
|
1133
1164
|
*/
|
|
@@ -1141,9 +1172,9 @@ declare class Element {
|
|
|
1141
1172
|
/** Lazily-decoded, memoized {@link DicomValue}. */
|
|
1142
1173
|
private _valueCache;
|
|
1143
1174
|
/**
|
|
1144
|
-
* Construct a new structural `Element`.
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1175
|
+
* Construct a new structural `Element`. The parser builds these directly
|
|
1176
|
+
* from on-wire bytes; consumers receive them via `Dataset` after
|
|
1177
|
+
* `parseDicom`.
|
|
1147
1178
|
*
|
|
1148
1179
|
* @internal
|
|
1149
1180
|
*/
|
|
@@ -1167,13 +1198,12 @@ declare class Element {
|
|
|
1167
1198
|
}
|
|
1168
1199
|
|
|
1169
1200
|
/**
|
|
1170
|
-
*
|
|
1201
|
+
* The structural `FileMeta` view-object shape.
|
|
1171
1202
|
*
|
|
1172
|
-
*
|
|
1173
|
-
*
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1176
|
-
* per D-19). Phase 3 may promote to a class if helpers are added.
|
|
1203
|
+
* A plain interface rather than a class, because it carries no methods.
|
|
1204
|
+
* Only `transferSyntaxUID` is required; the rest are populated by
|
|
1205
|
+
* `parseFileMeta` when present but never enforced (`validate()` is what
|
|
1206
|
+
* enforces File Meta Type-1 fields).
|
|
1177
1207
|
*
|
|
1178
1208
|
* @module
|
|
1179
1209
|
*/
|
|
@@ -1189,6 +1219,11 @@ declare class Element {
|
|
|
1189
1219
|
* on-wire value, even-length per PS3.5 §6.2) rather than dropped. `value` is a
|
|
1190
1220
|
* defensive copy, so the view never aliases the parsed input buffer.
|
|
1191
1221
|
*
|
|
1222
|
+
* 🛑 **THE ROUND TRIP IS PARSE-THEN-SERIALIZE ONLY. `deidentify()` DROPS EVERY
|
|
1223
|
+
* ELEMENT THIS TYPE DESCRIBES**, and the exact examples above are why: a Sending
|
|
1224
|
+
* AE Title and a Private Information pair are the identity information PS3.15
|
|
1225
|
+
* §E.1.1 names. See {@link FileMeta.extraElements} for the whole carve-out.
|
|
1226
|
+
*
|
|
1192
1227
|
* @example
|
|
1193
1228
|
* ```ts
|
|
1194
1229
|
* import { parseDicom } from "@cosyte/dicom";
|
|
@@ -1217,9 +1252,9 @@ interface FileMetaRawElement {
|
|
|
1217
1252
|
* over `(0002,xxxx)` elements parsed during the File Meta pre-pass.
|
|
1218
1253
|
*
|
|
1219
1254
|
* Only `transferSyntaxUID` is required because it is the dispatch input
|
|
1220
|
-
* for the
|
|
1255
|
+
* for the transfer-syntax parsers; everything else is optional
|
|
1221
1256
|
* because real-world clinical files routinely omit one or more
|
|
1222
|
-
* Type-1 elements.
|
|
1257
|
+
* Type-1 elements. `validate()` adds opinion-bearing checks
|
|
1223
1258
|
* for those missing elements.
|
|
1224
1259
|
*
|
|
1225
1260
|
* @example
|
|
@@ -1243,15 +1278,139 @@ interface FileMeta {
|
|
|
1243
1278
|
* Any `(0002,xxxx)` elements the source carried that the typed fields above
|
|
1244
1279
|
* do not model, preserved in tag order so a round-trip re-emits the File Meta
|
|
1245
1280
|
* group byte-for-byte. Omitted when the group held only modeled elements.
|
|
1281
|
+
*
|
|
1282
|
+
* ## 🛑 The byte-for-byte round trip does NOT hold for `deidentify()` output
|
|
1283
|
+
*
|
|
1284
|
+
* That promise is about **parse then serialize**, and it is scoped to it
|
|
1285
|
+
* deliberately rather than by omission. `deidentify()` rebuilds this group to
|
|
1286
|
+
* describe the de-identifying application instead of the source (PS3.15
|
|
1287
|
+
* §E.1.1: the File Meta Information "shall be replaced with a description of
|
|
1288
|
+
* the de-identifying application", because otherwise "identity information may
|
|
1289
|
+
* leak through unmodified File Meta Information ... includ[ing] information
|
|
1290
|
+
* regarding Application Entity Titles, Presentation Addresses, implementation
|
|
1291
|
+
* information, and private information"). So on the de-identify path this
|
|
1292
|
+
* array is **dropped in full** - not filtered, not sampled - and
|
|
1293
|
+
* `sourceApplicationEntityTitle` goes with it, while
|
|
1294
|
+
* `implementationClassUID` and `implementationVersionName` are replaced with
|
|
1295
|
+
* this library's own.
|
|
1296
|
+
*
|
|
1297
|
+
* The elements that survive are the ones that identify the **object** rather
|
|
1298
|
+
* than its sender: `fileMetaInformationVersion`, `mediaStorageSOPClassUID`,
|
|
1299
|
+
* `transferSyntaxUID`, and `mediaStorageSOPInstanceUID` (remapped unless
|
|
1300
|
+
* `RetainUIDs` is active). What was dropped is counted on the report
|
|
1301
|
+
* `deidentify()` returns, so a caller learns the fidelity loss from the run
|
|
1302
|
+
* rather than by diffing the bytes.
|
|
1303
|
+
*
|
|
1304
|
+
* A caller that needs the source group verbatim must read it off the
|
|
1305
|
+
* **parsed** dataset; it is not recoverable from de-identified output, and it
|
|
1306
|
+
* is not meant to be.
|
|
1246
1307
|
*/
|
|
1247
1308
|
readonly extraElements?: readonly FileMetaRawElement[];
|
|
1248
1309
|
}
|
|
1249
1310
|
|
|
1250
1311
|
/**
|
|
1251
|
-
*
|
|
1312
|
+
* The DICOMDIR Directory Record model: which records a DICOMDIR's offsets name,
|
|
1313
|
+
* and the tree they form.
|
|
1314
|
+
*
|
|
1315
|
+
* A DICOMDIR is a Dataset whose File Meta Media Storage SOP Class UID
|
|
1316
|
+
* `(0002,0002)` is `1.2.840.10008.1.3.10` (Media Storage Directory Storage). Its
|
|
1317
|
+
* Directory Record Sequence `(0004,1220)` holds one Item per record, and four
|
|
1318
|
+
* `UL` attributes link them by byte offset: `(0004,1200)` and `(0004,1202)` name
|
|
1319
|
+
* the first and last record of the root directory entity, and each record's
|
|
1320
|
+
* `(0004,1400)` names its next sibling and `(0004,1420)` the first record of its
|
|
1321
|
+
* lower-level entity. PS3.3 2026d Table F.3-3 counts each offset as "a number of
|
|
1322
|
+
* bytes starting with the first byte of the File Meta Information" and says it
|
|
1323
|
+
* "includes the File Preamble and the DICM Prefix"; zero means no record.
|
|
1324
|
+
*
|
|
1325
|
+
* **An offset resolves to a record only when it is exactly the file offset of
|
|
1326
|
+
* that record's `(FFFE,E000)` Item tag**, which the parser records on each Item
|
|
1327
|
+
* as {@link Item.fileOffset}. Nothing here scans the bytes for an Item tag: an
|
|
1328
|
+
* offset that lands on an Item of a Sequence nested inside a record, inside a
|
|
1329
|
+
* record's Data Set, on the Sequence's own header, or past the end of the file
|
|
1330
|
+
* names no record, and the parser warns (`DICOM_DIRECTORY_OFFSET_UNRESOLVED`).
|
|
1331
|
+
*
|
|
1332
|
+
* The analysis is one pure function of a Dataset, {@link analyzeDirectory}, read
|
|
1333
|
+
* three ways: `parseDicom` turns its findings into warnings, `Dataset.directory`
|
|
1334
|
+
* exposes its tree, and `serializeDicom` ties each offset it writes to a record
|
|
1335
|
+
* through the same {@link Item.fileOffset} identity.
|
|
1336
|
+
*
|
|
1337
|
+
* @module
|
|
1338
|
+
*/
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* One Directory Record of a DICOMDIR: an Item of the Directory Record Sequence
|
|
1342
|
+
* `(0004,1220)`, with the records its offsets name.
|
|
1343
|
+
*
|
|
1344
|
+
* @example
|
|
1345
|
+
* ```ts
|
|
1346
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
1347
|
+
* const dir = parseDicom(buf).directory;
|
|
1348
|
+
* for (const patient of dir?.root ?? []) {
|
|
1349
|
+
* patient.type; // "PATIENT"
|
|
1350
|
+
* for (const study of patient.lowerLevel) study.item.get("0020000D"); // Study Instance UID
|
|
1351
|
+
* }
|
|
1352
|
+
* ```
|
|
1353
|
+
*/
|
|
1354
|
+
interface DirectoryRecord {
|
|
1355
|
+
/** The record's 0-based index in the Directory Record Sequence. */
|
|
1356
|
+
readonly index: number;
|
|
1357
|
+
/**
|
|
1358
|
+
* The Directory Record Type `(0004,1430)` value (`"PATIENT"`, `"STUDY"`,
|
|
1359
|
+
* `"SERIES"`, `"IMAGE"` and the rest PS3.3 F.5 defines), or `undefined` when
|
|
1360
|
+
* the record carries none.
|
|
1361
|
+
*/
|
|
1362
|
+
readonly type: string | undefined;
|
|
1363
|
+
/**
|
|
1364
|
+
* The Referenced File ID `(0004,1500)`: its components in order (a path
|
|
1365
|
+
* relative to the File-set root, one component per value), as the file wrote
|
|
1366
|
+
* them less only the padding PS3.5 makes insignificant in `CS` (the trailing
|
|
1367
|
+
* pad, and each value's leading and trailing spaces). Kept verbatim: never
|
|
1368
|
+
* de-identified, rewritten or joined, and the element's own bytes on `item`
|
|
1369
|
+
* are untouched. `undefined` when the record carries none.
|
|
1370
|
+
*/
|
|
1371
|
+
readonly referencedFileId: readonly string[] | undefined;
|
|
1372
|
+
/**
|
|
1373
|
+
* The records of this record's lower-level directory entity: the one its
|
|
1374
|
+
* `(0004,1420)` names, then each `(0004,1400)` successor in order. Empty when
|
|
1375
|
+
* the offset is zero or names no record.
|
|
1376
|
+
*/
|
|
1377
|
+
readonly lowerLevel: readonly DirectoryRecord[];
|
|
1378
|
+
/** The record's own Data Set, for its record keys. */
|
|
1379
|
+
readonly item: Item;
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* The Directory Record tree of a DICOMDIR, read from its offsets.
|
|
1383
|
+
*
|
|
1384
|
+
* `records` is every Item of the Directory Record Sequence, in order, whether
|
|
1385
|
+
* or not an offset reaches it. `root` is the root directory entity: the record
|
|
1386
|
+
* `(0004,1200)` names, then each `(0004,1400)` successor in order. A record is
|
|
1387
|
+
* reached at most once: an offset that would reach one again is not followed.
|
|
1388
|
+
*
|
|
1389
|
+
* @example
|
|
1390
|
+
* ```ts
|
|
1391
|
+
* import { parseDicom, type DirectoryRecord } from "@cosyte/dicom";
|
|
1392
|
+
* const dir = parseDicom(buf).directory; // undefined unless the object is a DICOMDIR
|
|
1393
|
+
* const walk = (records: readonly DirectoryRecord[], depth = 0): void => {
|
|
1394
|
+
* for (const r of records) {
|
|
1395
|
+
* console.log(" ".repeat(depth), r.type, r.referencedFileId?.join("/"));
|
|
1396
|
+
* walk(r.lowerLevel, depth + 1);
|
|
1397
|
+
* }
|
|
1398
|
+
* };
|
|
1399
|
+
* if (dir !== undefined) walk(dir.root);
|
|
1400
|
+
* ```
|
|
1401
|
+
*/
|
|
1402
|
+
interface DicomDirectory {
|
|
1403
|
+
/** Every Directory Record, in Directory Record Sequence order. */
|
|
1404
|
+
readonly records: readonly DirectoryRecord[];
|
|
1405
|
+
/** The root directory entity, in `(0004,1400)` order. */
|
|
1406
|
+
readonly root: readonly DirectoryRecord[];
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* Typed view model for the safety-critical domain helpers
|
|
1252
1411
|
* (`Dataset.patient` / `study` / `series` / `image`).
|
|
1253
1412
|
*
|
|
1254
|
-
* Every field here obeys the
|
|
1413
|
+
* Every field here obeys the same fail-safe contract:
|
|
1255
1414
|
* a missing value is **typed-absent** (`undefined`), never a substituted
|
|
1256
1415
|
* default; a malformed numeric component stays `null` inside its array
|
|
1257
1416
|
* rather than being coerced; raw bytes are always still reachable via the
|
|
@@ -1285,7 +1444,7 @@ interface OtherPatientId {
|
|
|
1285
1444
|
readonly typeCode?: string;
|
|
1286
1445
|
}
|
|
1287
1446
|
/**
|
|
1288
|
-
* Patient & study identity (
|
|
1447
|
+
* Patient & study identity (the wrong-patient failure class).
|
|
1289
1448
|
*
|
|
1290
1449
|
* `id` is **not globally unique**; correct cross-system matching needs the
|
|
1291
1450
|
* `{id, issuerOfId, issuerQualifiers}` tuple plus `otherIds`. `name` keeps
|
|
@@ -1310,7 +1469,7 @@ interface PatientView {
|
|
|
1310
1469
|
readonly sex?: string;
|
|
1311
1470
|
}
|
|
1312
1471
|
/**
|
|
1313
|
-
* Study-level identity
|
|
1472
|
+
* Study-level identity. `instanceUid` is the cross-system study key;
|
|
1314
1473
|
* `accessionNumber` ties the study to the order.
|
|
1315
1474
|
*
|
|
1316
1475
|
* @example
|
|
@@ -1330,7 +1489,7 @@ interface StudyView {
|
|
|
1330
1489
|
readonly description?: string;
|
|
1331
1490
|
}
|
|
1332
1491
|
/**
|
|
1333
|
-
* Series-level identity & co-registration
|
|
1492
|
+
* Series-level identity & co-registration. Images sharing a
|
|
1334
1493
|
* `frameOfReferenceUid` are spatially co-registered.
|
|
1335
1494
|
*
|
|
1336
1495
|
* @example
|
|
@@ -1373,7 +1532,7 @@ interface CodedConcept {
|
|
|
1373
1532
|
readonly schemeUid?: string;
|
|
1374
1533
|
}
|
|
1375
1534
|
/**
|
|
1376
|
-
* A Real World Value Mapping
|
|
1535
|
+
* A Real World Value Mapping - slope/intercept bound atomically to
|
|
1377
1536
|
* its UCUM measurement-units code, so a number is never detached from its
|
|
1378
1537
|
* units. From Real World Value Mapping Sequence `(0040,9096)`.
|
|
1379
1538
|
*
|
|
@@ -1390,7 +1549,7 @@ interface RealWorldValueMap {
|
|
|
1390
1549
|
}
|
|
1391
1550
|
/**
|
|
1392
1551
|
* The five Enhanced multi-frame functional-group macros, resolved for a
|
|
1393
|
-
* single frame Per-Frame-else-Shared (
|
|
1552
|
+
* single frame Per-Frame-else-Shared (PS3.3 §C.7.6.16). Each macro is
|
|
1394
1553
|
* typed-absent when present in neither the per-frame nor the shared group
|
|
1395
1554
|
* (the three *geometry* macros are treated as required for an enhanced
|
|
1396
1555
|
* object - see {@link ImageView.frame}).
|
|
@@ -1427,7 +1586,7 @@ interface FrameFunctionalGroups {
|
|
|
1427
1586
|
};
|
|
1428
1587
|
}
|
|
1429
1588
|
/**
|
|
1430
|
-
* Pixel-interpretation + geometry metadata
|
|
1589
|
+
* Pixel-interpretation + geometry metadata - the
|
|
1431
1590
|
* "wrong pixels look fine" and "looks fine, measures wrong" classes. v1 does
|
|
1432
1591
|
* not decode pixels; this view surfaces exactly what a renderer needs so it
|
|
1433
1592
|
* does not have to guess.
|
|
@@ -1492,14 +1651,13 @@ interface ImageView {
|
|
|
1492
1651
|
}
|
|
1493
1652
|
|
|
1494
1653
|
/**
|
|
1495
|
-
*
|
|
1654
|
+
* The structural `Dataset` - top-level container produced by
|
|
1496
1655
|
* `parseDicom`.
|
|
1497
1656
|
*
|
|
1498
|
-
*
|
|
1499
|
-
*
|
|
1500
|
-
*
|
|
1501
|
-
*
|
|
1502
|
-
* surface.
|
|
1657
|
+
* Only `fileMeta` and `warnings` are public data; the element map is held
|
|
1658
|
+
* internally and reached through the `get` / `has` / `elements` / `getAll` /
|
|
1659
|
+
* `setElement` / `addElement` / `removeElement` / `addItem` / `removeItem`
|
|
1660
|
+
* navigation surface.
|
|
1503
1661
|
*
|
|
1504
1662
|
* The `warnings` array is frozen at the constructor boundary (mirrors
|
|
1505
1663
|
* `@cosyte/hl7` sibling `model/message.ts`) so consumers receive
|
|
@@ -1523,12 +1681,11 @@ interface DatasetInit {
|
|
|
1523
1681
|
readonly elements: ReadonlyMap<Tag, Element>;
|
|
1524
1682
|
}
|
|
1525
1683
|
/**
|
|
1526
|
-
* One parsed DICOM dataset - the structural shell
|
|
1684
|
+
* One parsed DICOM dataset - the structural shell.
|
|
1527
1685
|
*
|
|
1528
|
-
*
|
|
1529
|
-
*
|
|
1530
|
-
*
|
|
1531
|
-
* D-42.
|
|
1686
|
+
* Public data: `fileMeta`, `warnings`. The internal element map is stored
|
|
1687
|
+
* on `_elements` (protected) and reached through the public
|
|
1688
|
+
* `get` / `has` / `elements` / `getAll` navigation API.
|
|
1532
1689
|
*
|
|
1533
1690
|
* @example
|
|
1534
1691
|
* ```ts
|
|
@@ -1544,21 +1701,23 @@ declare class Dataset {
|
|
|
1544
1701
|
readonly fileMeta: FileMeta | undefined;
|
|
1545
1702
|
readonly warnings: readonly DicomParseWarning[];
|
|
1546
1703
|
/**
|
|
1547
|
-
* Element map, keyed by uppercase 8-hex tag.
|
|
1548
|
-
*
|
|
1549
|
-
*
|
|
1704
|
+
* Element map, keyed by uppercase 8-hex tag. Reached publicly through the
|
|
1705
|
+
* navigation surface; kept `protected` here so only subclasses (`Item`, and
|
|
1706
|
+
* any later extension) can introspect it directly.
|
|
1550
1707
|
*
|
|
1551
1708
|
* @internal
|
|
1552
1709
|
*/
|
|
1553
1710
|
protected readonly _elements: ReadonlyMap<Tag, Element>;
|
|
1554
|
-
/** Memoised
|
|
1711
|
+
/** Memoised domain views (built once, on first access). */
|
|
1555
1712
|
private _patient?;
|
|
1556
1713
|
private _study?;
|
|
1557
1714
|
private _series?;
|
|
1558
1715
|
private _image?;
|
|
1716
|
+
/** Boxed so a memoised "not a DICOMDIR" is told apart from "not read yet". */
|
|
1717
|
+
private _directory?;
|
|
1559
1718
|
/**
|
|
1560
|
-
* Construct a new structural `Dataset`.
|
|
1561
|
-
*
|
|
1719
|
+
* Construct a new structural `Dataset`. The warnings array is frozen at
|
|
1720
|
+
* the model boundary.
|
|
1562
1721
|
*
|
|
1563
1722
|
* @internal
|
|
1564
1723
|
*/
|
|
@@ -1614,7 +1773,7 @@ declare class Dataset {
|
|
|
1614
1773
|
*/
|
|
1615
1774
|
getAll(tag: Tag): readonly Element[];
|
|
1616
1775
|
/**
|
|
1617
|
-
* Patient-identity view
|
|
1776
|
+
* Patient-identity view. Fail-safe typed-absent fields; `id` is
|
|
1618
1777
|
* **not** globally unique - match on the `{id, issuerOfId, ...}` tuple
|
|
1619
1778
|
* plus `otherIds`, never on a bare `(0010,0020)`. Memoised on first read.
|
|
1620
1779
|
*
|
|
@@ -1627,7 +1786,7 @@ declare class Dataset {
|
|
|
1627
1786
|
*/
|
|
1628
1787
|
get patient(): PatientView;
|
|
1629
1788
|
/**
|
|
1630
|
-
* Study-identity view
|
|
1789
|
+
* Study-identity view. `instanceUid` is the cross-system study
|
|
1631
1790
|
* key; `accessionNumber` ties it to the order. Memoised on first read.
|
|
1632
1791
|
*
|
|
1633
1792
|
* @example
|
|
@@ -1639,7 +1798,7 @@ declare class Dataset {
|
|
|
1639
1798
|
*/
|
|
1640
1799
|
get study(): StudyView;
|
|
1641
1800
|
/**
|
|
1642
|
-
* Series-identity & co-registration view
|
|
1801
|
+
* Series-identity & co-registration view. A shared
|
|
1643
1802
|
* `frameOfReferenceUid` means images are spatially co-registered.
|
|
1644
1803
|
* Memoised on first read.
|
|
1645
1804
|
*
|
|
@@ -1652,7 +1811,7 @@ declare class Dataset {
|
|
|
1652
1811
|
*/
|
|
1653
1812
|
get series(): SeriesView;
|
|
1654
1813
|
/**
|
|
1655
|
-
* Pixel-interpretation + geometry view
|
|
1814
|
+
* Pixel-interpretation + geometry view. Surfaces exactly
|
|
1656
1815
|
* what a renderer needs without guessing: `rescaleSlope`/`signed`/
|
|
1657
1816
|
* `photometricInterpretation` stay absent rather than defaulted, and the
|
|
1658
1817
|
* three pixel-spacing tags are distinct. Memoised on first read.
|
|
@@ -1665,26 +1824,59 @@ declare class Dataset {
|
|
|
1665
1824
|
* ```
|
|
1666
1825
|
*/
|
|
1667
1826
|
get image(): ImageView;
|
|
1827
|
+
/**
|
|
1828
|
+
* The DICOMDIR Directory Record tree, or `undefined` when this Dataset is not
|
|
1829
|
+
* a DICOMDIR (its File Meta Media Storage SOP Class UID is not
|
|
1830
|
+
* `1.2.840.10008.1.3.10`). Read from the four offset attributes (PS3.3 2026d
|
|
1831
|
+
* Table F.3-3): `root` is the record `(0004,1200)` names and each
|
|
1832
|
+
* `(0004,1400)` successor, and each record's `lowerLevel` is the record its
|
|
1833
|
+
* `(0004,1420)` names and each successor. An offset resolves only when it is
|
|
1834
|
+
* exactly the file offset of an Item of the Directory Record Sequence
|
|
1835
|
+
* `(0004,1220)`; one that is not names no record, and `parseDicom` warns
|
|
1836
|
+
* (`DICOM_DIRECTORY_OFFSET_UNRESOLVED`, `DICOM_DIRECTORY_OFFSET_MALFORMED`,
|
|
1837
|
+
* `DICOM_DIRECTORY_RECORD_REVISITED`, `DICOM_DIRECTORY_OFFSET_DEFLATED`).
|
|
1838
|
+
* Memoised on first read.
|
|
1839
|
+
*
|
|
1840
|
+
* Limits: record keys are not checked against PS3.3 F.5, the File-set
|
|
1841
|
+
* Consistency Flag and `(0004,1202)`'s agreement with the end of the root
|
|
1842
|
+
* chain are not checked, and under Deflated Explicit VR LE no offset resolves,
|
|
1843
|
+
* so every record is listed in `records` and none is linked.
|
|
1844
|
+
*
|
|
1845
|
+
* @example
|
|
1846
|
+
* ```ts
|
|
1847
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
1848
|
+
* const dir = parseDicom(buf).directory;
|
|
1849
|
+
* for (const patient of dir?.root ?? []) {
|
|
1850
|
+
* for (const study of patient.lowerLevel) {
|
|
1851
|
+
* for (const series of study.lowerLevel) {
|
|
1852
|
+
* for (const image of series.lowerLevel) console.log(image.referencedFileId);
|
|
1853
|
+
* }
|
|
1854
|
+
* }
|
|
1855
|
+
* }
|
|
1856
|
+
* ```
|
|
1857
|
+
*/
|
|
1858
|
+
get directory(): DicomDirectory | undefined;
|
|
1668
1859
|
}
|
|
1669
1860
|
|
|
1670
1861
|
/**
|
|
1671
1862
|
* Public parser entry - `parseDicom`.
|
|
1672
1863
|
*
|
|
1673
|
-
* Pipeline
|
|
1674
|
-
* 1. Dual EMPTY_INPUT check (raw input + post-normalize)
|
|
1864
|
+
* Pipeline:
|
|
1865
|
+
* 1. Dual EMPTY_INPUT check (raw input + post-normalize).
|
|
1675
1866
|
* 2. {@link normalizeInput} converts `Buffer | Uint8Array | ArrayBuffer`
|
|
1676
|
-
* to a zero-copy `Buffer` view
|
|
1677
|
-
* 3. {@link parsePart10Header} resolves preamble + DICM framing
|
|
1867
|
+
* to a zero-copy `Buffer` view.
|
|
1868
|
+
* 3. {@link parsePart10Header} resolves preamble + DICM framing.
|
|
1678
1869
|
* 4. {@link parseFileMeta} parses the (always Explicit VR LE) File Meta
|
|
1679
|
-
* group
|
|
1680
|
-
* 5. {@link TRANSFER_SYNTAX_PARSERS} dispatches on TS UID
|
|
1870
|
+
* group.
|
|
1871
|
+
* 5. {@link TRANSFER_SYNTAX_PARSERS} dispatches on TS UID;
|
|
1681
1872
|
* unsupported UIDs throw `UNSUPPORTED_TRANSFER_SYNTAX` carrying the
|
|
1682
1873
|
* `Dictionary.uid(uid)?.name` in `err.snippet`.
|
|
1683
|
-
* 6. The chosen strategy returns a `ReadonlyMap<Tag, Element
|
|
1684
|
-
*
|
|
1685
|
-
*
|
|
1874
|
+
* 6. The chosen strategy returns a `ReadonlyMap<Tag, Element>`.
|
|
1875
|
+
* 7. For a DICOMDIR, every Directory Record offset is resolved against the
|
|
1876
|
+
* Items' file offsets and each one that names no record is warned.
|
|
1877
|
+
* 8. The result is assembled into a structural {@link Dataset}.
|
|
1686
1878
|
*
|
|
1687
|
-
*
|
|
1879
|
+
* A source/vendor `Profile` is wired in through `ParseOptions.profile`:
|
|
1688
1880
|
* its `escalations` / `suppressions` reshape Tier-2 emission at the
|
|
1689
1881
|
* {@link makeEmitter} chokepoint, and its private-dictionary overlay resolves
|
|
1690
1882
|
* the Implicit VR of vendor private data elements.
|
|
@@ -1705,8 +1897,28 @@ declare class Dataset {
|
|
|
1705
1897
|
* a recognizable `(0002,0000)` File Meta Group Length at offset 0.
|
|
1706
1898
|
* - `INVALID_FILE_META` - File Meta is truncated or `(0002,0010)`
|
|
1707
1899
|
* Transfer Syntax UID is missing.
|
|
1708
|
-
* - `UNSUPPORTED_TRANSFER_SYNTAX` - Transfer Syntax UID is
|
|
1709
|
-
* four
|
|
1900
|
+
* - `UNSUPPORTED_TRANSFER_SYNTAX` - Transfer Syntax UID is none of the
|
|
1901
|
+
* supported ones: the four native syntaxes (`1.2.840.10008.1.2`,
|
|
1902
|
+
* `…1.2.1`, `…1.2.2`, `…1.2.1.99`), every encapsulated Pixel Data
|
|
1903
|
+
* syntax PS3.5 2026c section A.4 names (JPEG, JPEG-LS, JPEG 2000, HTJ2K,
|
|
1904
|
+
* RLE, MPEG, HEVC, JPEG XL, Deflated Image Frame, Encapsulated
|
|
1905
|
+
* Uncompressed), and the four JPIP Referenced syntaxes of sections A.6,
|
|
1906
|
+
* A.7, A.11 and A.12. The SMPTE ST 2110 syntaxes and every retired UID
|
|
1907
|
+
* stay refused.
|
|
1908
|
+
*
|
|
1909
|
+
* An object under a section A.4 syntax is read under Explicit VR Little Endian
|
|
1910
|
+
* rules, as A.4 requires: its metadata is fully readable, and its Pixel Data
|
|
1911
|
+
* fragments are handed back as opaque, undecoded bytes by
|
|
1912
|
+
* {@link readPixelDataFragments}. **No pixel is ever decoded.**
|
|
1913
|
+
*
|
|
1914
|
+
* An object under a JPIP Referenced syntax is read the same way for metadata:
|
|
1915
|
+
* sections A.6 and A.11 make the Data Set Explicit VR Little Endian, and
|
|
1916
|
+
* sections A.7 and A.12 deflate that Data Set per RFC 1951, so it is inflated
|
|
1917
|
+
* first under the Deflated reader's decompression cap. Its Pixel Data Provider
|
|
1918
|
+
* URL (0028,7FE0) is returned as the `UR` element the file carries and is
|
|
1919
|
+
* **never fetched, resolved or validated**. The object is read-only here:
|
|
1920
|
+
* `serializeDicom` refuses to write the four JPIP syntaxes, and `deidentify`
|
|
1921
|
+
* refuses to de-identify an object under one of them.
|
|
1710
1922
|
*
|
|
1711
1923
|
* Pass `{ strict: true }` to escalate every Tier-2 warning to a thrown
|
|
1712
1924
|
* `DicomParseError` carrying the warning code.
|
|
@@ -1716,7 +1928,7 @@ declare class Dataset {
|
|
|
1716
1928
|
* import { parseDicom, WARNING_CODES, DicomParseError } from "@cosyte/dicom";
|
|
1717
1929
|
* import { readFileSync } from "node:fs";
|
|
1718
1930
|
*
|
|
1719
|
-
* // Three input shapes
|
|
1931
|
+
* // Three input shapes: Buffer, Uint8Array, ArrayBuffer.
|
|
1720
1932
|
* const bytes = readFileSync("study.dcm");
|
|
1721
1933
|
* const ds1 = parseDicom(bytes);
|
|
1722
1934
|
* const ds2 = parseDicom(new Uint8Array(bytes));
|
|
@@ -1747,11 +1959,11 @@ declare function parseDicom(input: Buffer | Uint8Array | ArrayBuffer): Dataset;
|
|
|
1747
1959
|
declare function parseDicom(input: Buffer | Uint8Array | ArrayBuffer, options: ParseOptions): Dataset;
|
|
1748
1960
|
|
|
1749
1961
|
/**
|
|
1750
|
-
*
|
|
1962
|
+
* The structural `Sequence` - wrapper around an SQ element's items.
|
|
1751
1963
|
*
|
|
1752
|
-
*
|
|
1753
|
-
*
|
|
1754
|
-
*
|
|
1964
|
+
* Structural surface only: `items` and `length`. No navigation methods of
|
|
1965
|
+
* its own; `Sequence.items[N].get(...)` comes from `Item`'s `Dataset`
|
|
1966
|
+
* superclass.
|
|
1755
1967
|
*
|
|
1756
1968
|
* @module
|
|
1757
1969
|
*/
|
|
@@ -1760,12 +1972,12 @@ declare function parseDicom(input: Buffer | Uint8Array | ArrayBuffer, options: P
|
|
|
1760
1972
|
* One SQ (Sequence) element's structural body.
|
|
1761
1973
|
*
|
|
1762
1974
|
* `length` is the on-wire length: a real byte count for defined-length
|
|
1763
|
-
* SQ, or `0xFFFFFFFF` (4_294_967_295) when undefined-length
|
|
1975
|
+
* SQ, or `0xFFFFFFFF` (4_294_967_295) when undefined-length.
|
|
1764
1976
|
*
|
|
1765
1977
|
* @example
|
|
1766
1978
|
* ```ts
|
|
1767
1979
|
* import { Sequence } from "@cosyte/dicom";
|
|
1768
|
-
* //
|
|
1980
|
+
* // The parser constructs sequences as follows:
|
|
1769
1981
|
* // const sq = new Sequence([item0, item1], 0xFFFFFFFF);
|
|
1770
1982
|
* ```
|
|
1771
1983
|
*/
|
|
@@ -1781,6 +1993,99 @@ declare class Sequence {
|
|
|
1781
1993
|
constructor(items: readonly Item[], length: number);
|
|
1782
1994
|
}
|
|
1783
1995
|
|
|
1996
|
+
/**
|
|
1997
|
+
* Encapsulated Pixel Data, handed back as raw bytes: the Basic Offset Table and
|
|
1998
|
+
* every fragment, exactly as the file carries them.
|
|
1999
|
+
*
|
|
2000
|
+
* An object under a PS3.5 2026c section A.4 Transfer Syntax (JPEG, JPEG-LS,
|
|
2001
|
+
* JPEG 2000, HTJ2K, RLE and the rest) carries its Pixel Data `(7FE0,0010)` as an
|
|
2002
|
+
* Item stream: a Basic Offset Table Item, then one Item per fragment of the
|
|
2003
|
+
* encoded pixel stream, then a Sequence Delimitation Item. The parser reads that
|
|
2004
|
+
* structure and never reads a fragment's content. This module is how a caller
|
|
2005
|
+
* gets the content: as opaque, **undecoded** bytes. No pixel is decompressed, no
|
|
2006
|
+
* frame is assembled from fragments, and the Basic Offset Table's offsets are
|
|
2007
|
+
* not interpreted, because each of those is a codec's or a viewer's decision and
|
|
2008
|
+
* this package decodes no pixels.
|
|
2009
|
+
*
|
|
2010
|
+
* @module
|
|
2011
|
+
*/
|
|
2012
|
+
|
|
2013
|
+
/**
|
|
2014
|
+
* The Basic Offset Table and the fragments of one encapsulated Pixel Data
|
|
2015
|
+
* element, each as the raw bytes of its Item Value.
|
|
2016
|
+
*
|
|
2017
|
+
* Every `Buffer` here is a view over the element's own `Element.rawBytes`, with
|
|
2018
|
+
* no Item header and no Sequence Delimitation Item in it, so it follows that
|
|
2019
|
+
* element's retention: a zero-copy view over your input by default, and a copy
|
|
2020
|
+
* that outlives it under `parseDicom(bytes, { copyValues: true })`.
|
|
2021
|
+
*
|
|
2022
|
+
* @example
|
|
2023
|
+
* ```ts
|
|
2024
|
+
* import { parseDicom, readPixelDataFragments } from "@cosyte/dicom";
|
|
2025
|
+
* const pixels = readPixelDataFragments(parseDicom(buf));
|
|
2026
|
+
* if (pixels !== undefined) {
|
|
2027
|
+
* pixels.basicOffsetTable?.length; // 0 when the table is empty
|
|
2028
|
+
* pixels.fragments.length; // how many fragment Items the file carries
|
|
2029
|
+
* }
|
|
2030
|
+
* ```
|
|
2031
|
+
*/
|
|
2032
|
+
interface PixelDataFragments {
|
|
2033
|
+
/**
|
|
2034
|
+
* The Basic Offset Table's Item Value, uninterpreted. **Zero bytes when the
|
|
2035
|
+
* table is empty**, which PS3.5 2026c section A.4 permits and decoders must
|
|
2036
|
+
* accept. `undefined` only when the Item stream holds no Item at all, which
|
|
2037
|
+
* A.4 does not permit; nothing is invented in its place.
|
|
2038
|
+
*/
|
|
2039
|
+
readonly basicOffsetTable: Buffer | undefined;
|
|
2040
|
+
/**
|
|
2041
|
+
* The Item Value of every Item after the Basic Offset Table, in file order.
|
|
2042
|
+
* Each is returned whole: its content is never read, so bytes inside a
|
|
2043
|
+
* fragment that happen to look like a delimiter are fragment bytes (A.4: a
|
|
2044
|
+
* decoder "may not scan for a Sequence Delimitation Item").
|
|
2045
|
+
*/
|
|
2046
|
+
readonly fragments: readonly Buffer[];
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Read the Basic Offset Table and every fragment of a Data Set's encapsulated
|
|
2050
|
+
* Pixel Data `(7FE0,0010)` as raw bytes, without decoding anything.
|
|
2051
|
+
*
|
|
2052
|
+
* Pass the root {@link Dataset} `parseDicom` returned: its Pixel Data is the
|
|
2053
|
+
* top-level element PS3.5 2026c section A.4 encapsulates. An `Item` is a
|
|
2054
|
+
* `Dataset` too, and is read the same way over its own elements.
|
|
2055
|
+
*
|
|
2056
|
+
* **`undefined` means this Data Set has no encapsulated fragments**: Pixel Data
|
|
2057
|
+
* is absent, or it is present in native format with an explicit Value Length.
|
|
2058
|
+
* That is never an empty list and never a Basic Offset Table, so it cannot be
|
|
2059
|
+
* mistaken for an encapsulated result.
|
|
2060
|
+
*
|
|
2061
|
+
* **The list can be short, and the parse says so.** A stream that ends with the
|
|
2062
|
+
* input before its Sequence Delimitation Item still parses, and
|
|
2063
|
+
* `DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED` on `ds.warnings` says that the
|
|
2064
|
+
* fragments read may not be all the sender wrote. Read the warnings before
|
|
2065
|
+
* treating the list as complete.
|
|
2066
|
+
*
|
|
2067
|
+
* @throws `DicomParseError` with code `INVALID_FILE_META` when the element's
|
|
2068
|
+
* bytes are not an Item stream (an Item header cut short, an Item reaching past
|
|
2069
|
+
* the bytes, or a tag other than `(FFFE,E000)` / `(FFFE,E0DD)`). `parseDicom`
|
|
2070
|
+
* refuses such bytes itself, so only a hand-built `Element` reaches this; the
|
|
2071
|
+
* error carries no fragment byte and no length outside its 16-byte `snippet`.
|
|
2072
|
+
*
|
|
2073
|
+
* @example
|
|
2074
|
+
* ```ts
|
|
2075
|
+
* import { parseDicom, readPixelDataFragments } from "@cosyte/dicom";
|
|
2076
|
+
* const ds = parseDicom(buf); // e.g. a JPEG 2000 object
|
|
2077
|
+
* const pixels = readPixelDataFragments(ds);
|
|
2078
|
+
* if (pixels === undefined) {
|
|
2079
|
+
* // no encapsulated Pixel Data: absent, or native
|
|
2080
|
+
* } else {
|
|
2081
|
+
* for (const fragment of pixels.fragments) {
|
|
2082
|
+
* // hand `fragment` to a codec of your choice; it is the bytes as written
|
|
2083
|
+
* }
|
|
2084
|
+
* }
|
|
2085
|
+
* ```
|
|
2086
|
+
*/
|
|
2087
|
+
declare function readPixelDataFragments(ds: Dataset): PixelDataFragments | undefined;
|
|
2088
|
+
|
|
1784
2089
|
/**
|
|
1785
2090
|
* VR-aware value dispatch - the body behind `Element.value`.
|
|
1786
2091
|
*
|
|
@@ -1795,6 +2100,11 @@ declare class Sequence {
|
|
|
1795
2100
|
* - **Charset**: `LO SH UC LT ST UT PN` decode through `(0008,0005)`;
|
|
1796
2101
|
* the remaining string VRs are the Default Repertoire (ASCII, decoded
|
|
1797
2102
|
* Latin-1-lenient with `DICOM_NON_ASCII_IN_ASCII_VR` on stray bytes).
|
|
2103
|
+
* A multi-valued `(0008,0005)` decodes with ISO 2022 code extensions
|
|
2104
|
+
* (`./iso2022`), resetting to Value 1 at the VR's own delimiters, and
|
|
2105
|
+
* flags `DICOM_CHARSET_ESCAPE_UNDECLARED`,
|
|
2106
|
+
* `DICOM_CHARSET_BYTES_UNDECODABLE` and
|
|
2107
|
+
* `DICOM_CHARSET_EXTENSION_NOT_RESET` at most once each per value.
|
|
1798
2108
|
* - **Numeric strings** `DS`/`IS` → `number | null` (never `NaN`→0).
|
|
1799
2109
|
* - **Temporal** `DA`/`TM`/`DT` → tolerant typed values (warn + preserve).
|
|
1800
2110
|
* - **Bulk** `OB OD OF OL OV OW UN` → raw `binary` (not interpreted in v1).
|
|
@@ -1825,11 +2135,12 @@ declare function decodeElementValue(element: Element): DicomValue;
|
|
|
1825
2135
|
|
|
1826
2136
|
/**
|
|
1827
2137
|
* Specific Character Set `(0008,0005)` resolution + text decoding for the
|
|
1828
|
-
*
|
|
2138
|
+
* VR value decoders.
|
|
1829
2139
|
*
|
|
1830
2140
|
* Zero runtime deps: decoding rides on Node 22's full-ICU `TextDecoder`
|
|
1831
2141
|
* (the `@cosyte/dicom` ≤3-dep ceiling keeps `iconv-lite` in reserve only if
|
|
1832
|
-
* a future
|
|
2142
|
+
* a future CJK case forces it). A multi-valued set's ISO 2022 code
|
|
2143
|
+
* extensions are decoded by `./iso2022`, set by set, on the same labels.
|
|
1833
2144
|
*
|
|
1834
2145
|
* Term-list corrections verified against PS3.3 §C.12.1.1.2 for this build
|
|
1835
2146
|
* (the easy-to-get-wrong ones):
|
|
@@ -1885,6 +2196,10 @@ declare function isKnownCharsetTerm(term: string): boolean;
|
|
|
1885
2196
|
* the term list mixes single-byte and code-extension sets; falls back to
|
|
1886
2197
|
* the default repertoire (Latin-1, lenient) when no term resolves.
|
|
1887
2198
|
*
|
|
2199
|
+
* {@link decodeText} decodes under this label when the list has one term, or
|
|
2200
|
+
* when Value 1 is `ISO_IR 192`, `GB18030` or `GBK`. Any other multi-term list
|
|
2201
|
+
* decodes with ISO 2022 code extensions instead, whatever label this returns.
|
|
2202
|
+
*
|
|
1888
2203
|
* @example
|
|
1889
2204
|
* ```ts
|
|
1890
2205
|
* import { resolveDecoderLabel } from "@cosyte/dicom";
|
|
@@ -1894,14 +2209,26 @@ declare function isKnownCharsetTerm(term: string): boolean;
|
|
|
1894
2209
|
*/
|
|
1895
2210
|
declare function resolveDecoderLabel(terms: readonly string[] | undefined): string;
|
|
1896
2211
|
/**
|
|
1897
|
-
* Decode bytes to a string under the
|
|
1898
|
-
*
|
|
1899
|
-
*
|
|
2212
|
+
* Decode bytes to a string under the Specific Character Set terms, never
|
|
2213
|
+
* throwing.
|
|
2214
|
+
*
|
|
2215
|
+
* With more than one term, and a Value 1 other than `ISO_IR 192`, `GB18030` or
|
|
2216
|
+
* `GBK`, the bytes decode with ISO 2022 code extensions (PS3.5 section
|
|
2217
|
+
* 6.1.2.5): every PS3.3 Table C.12-3 and C.12-4 escape sequence switches G0 or
|
|
2218
|
+
* G1, no escape byte reaches the string, and the Value 1 designations come back
|
|
2219
|
+
* after each CR, LF or FF, which are the reset points of a text VR. Bytes no
|
|
2220
|
+
* designated set decodes read as U+FFFD. Only the string is returned here;
|
|
2221
|
+
* `Element.value` carries the warnings for an element's value.
|
|
2222
|
+
*
|
|
2223
|
+
* Every other term list decodes under {@link resolveDecoderLabel}'s label: an
|
|
2224
|
+
* unsupported label falls back to UTF-8, then to Latin-1 (1:1, cannot fail).
|
|
1900
2225
|
*
|
|
1901
2226
|
* @example
|
|
1902
2227
|
* ```ts
|
|
1903
2228
|
* import { decodeText } from "@cosyte/dicom";
|
|
1904
2229
|
* const s = decodeText(buf, ["ISO_IR 192"]); // UTF-8
|
|
2230
|
+
* // ESC $ B, the JIS X 0208 bytes of one character, ESC ( B
|
|
2231
|
+
* decodeText(Buffer.from([0x1b, 0x24, 0x42, 0x3b, 0x33, 0x1b, 0x28, 0x42]), ["", "ISO 2022 IR 87"]);
|
|
1905
2232
|
* ```
|
|
1906
2233
|
*/
|
|
1907
2234
|
declare function decodeText(bytes: Buffer, terms: readonly string[] | undefined): string;
|
|
@@ -1992,7 +2319,209 @@ declare function parseDateTime(raw: string): {
|
|
|
1992
2319
|
};
|
|
1993
2320
|
|
|
1994
2321
|
/**
|
|
1995
|
-
*
|
|
2322
|
+
* Calendar-component conversion for the decoded `DA` / `TM` / `DT` values.
|
|
2323
|
+
*
|
|
2324
|
+
* `parseDate`, `parseTime` and `parseDateTime` decode three separate wire
|
|
2325
|
+
* syntaxes into three separate shapes. These three functions project all of
|
|
2326
|
+
* them onto one: `toObject` for the calendar components, `toISO` for a string,
|
|
2327
|
+
* `toDate` for an absolute instant. The same three names, with the same
|
|
2328
|
+
* meanings, are exported by every `@cosyte/*` parser that decodes a date, so a
|
|
2329
|
+
* caller that learns them once can read a timestamp out of any of them.
|
|
2330
|
+
*
|
|
2331
|
+
* Three properties are the whole point of the surface and are asserted rather
|
|
2332
|
+
* than assumed in `test/conversion-surface.test.ts`:
|
|
2333
|
+
*
|
|
2334
|
+
* - **A component the value did not state is ABSENT.** Nothing is zero-filled,
|
|
2335
|
+
* so `Object.keys()` of the result is exactly the precision the sender wrote.
|
|
2336
|
+
* - **`millisecond` comes from the digits in `raw`, never from
|
|
2337
|
+
* `fractionalSeconds`.** `DicomTime.fractionalSeconds` is a binary float, and
|
|
2338
|
+
* the standard puts no bound on how many digits a `TM` may carry, so the
|
|
2339
|
+
* digits are read as written and the float is not consulted at all. `toISO`
|
|
2340
|
+
* renders the same digits verbatim, neither padded to three nor rounded.
|
|
2341
|
+
* - **`toDate` never guesses a zone.** A value with no `&ZZXX` offset and no
|
|
2342
|
+
* `assumeOffsetMinutes` is not an instant, so it answers `undefined` rather
|
|
2343
|
+
* than reading the host machine's zone.
|
|
2344
|
+
* - **A component outside its calendar range is REFUSED, never rolled over.**
|
|
2345
|
+
* The decoders bound the day at 1 to 31 without consulting the month, so
|
|
2346
|
+
* `parseDate("20240230")` is `valid: true`. Projecting that yields
|
|
2347
|
+
* `"2024-02-30"`, which every ISO-8601 reader silently moves to 1 March, and
|
|
2348
|
+
* an instant a day after the one the sender wrote. PatientBirthDate
|
|
2349
|
+
* (0010,0030) is a `DA`, so the harm is a date of birth shifted by a day with
|
|
2350
|
+
* nothing thrown. All three functions answer `undefined` instead.
|
|
2351
|
+
*
|
|
2352
|
+
* None of the three throws, for any input.
|
|
2353
|
+
*
|
|
2354
|
+
* The refusal lives HERE and not in `parseDate` / `parseTime` / `parseDateTime`:
|
|
2355
|
+
* those keep the behaviour they were pinned at, so a caller that wants the
|
|
2356
|
+
* sender's bytes still reads them off the decoded value and off `raw`. What is
|
|
2357
|
+
* refused is the PROJECTION of a value onto a calendar that has no such day.
|
|
2358
|
+
*
|
|
2359
|
+
* @module
|
|
2360
|
+
*/
|
|
2361
|
+
|
|
2362
|
+
/**
|
|
2363
|
+
* The decoded temporal values this surface accepts: a `DA`, a `TM` or a `DT`.
|
|
2364
|
+
*
|
|
2365
|
+
* Deliberately not exported. The three members are exported types a caller can
|
|
2366
|
+
* already name, and the Contract these functions implement introduces no new
|
|
2367
|
+
* value type in any package that adopts it.
|
|
2368
|
+
*/
|
|
2369
|
+
type DicomTemporal = DicomDate | DicomTime | DicomDateTime;
|
|
2370
|
+
/**
|
|
2371
|
+
* The calendar components a temporal value actually stated, and nothing else.
|
|
2372
|
+
*
|
|
2373
|
+
* Every value is a number, `month` is 1 to 12 (never the JS `Date` 0 to 11),
|
|
2374
|
+
* and a component the value did not state is ABSENT rather than present and
|
|
2375
|
+
* `undefined`: there is no `precision` key because the key set is the
|
|
2376
|
+
* precision. No `raw`, no `valid` and no parse bookkeeping reaches it.
|
|
2377
|
+
*
|
|
2378
|
+
* The shape is the one `Temporal.PlainDateTime.from` and luxon's
|
|
2379
|
+
* `DateTime.fromObject` accept, which is why the names are singular where
|
|
2380
|
+
* {@link DicomTime} and {@link DicomDateTime} spell them plurally. Delete
|
|
2381
|
+
* `offsetMinutes` and either constructor takes the rest with no key rename and
|
|
2382
|
+
* no value adjustment. That is a documented property rather than a tested one:
|
|
2383
|
+
* proving it would mean taking a dependency, and this package takes none for it.
|
|
2384
|
+
*
|
|
2385
|
+
* @example
|
|
2386
|
+
* ```ts
|
|
2387
|
+
* import { parseDateTime, toObject } from "@cosyte/dicom";
|
|
2388
|
+
* import type { DateParts } from "@cosyte/dicom";
|
|
2389
|
+
*
|
|
2390
|
+
* const parts: DateParts | undefined = toObject(parseDateTime("202401151330").value);
|
|
2391
|
+
* Object.keys(parts ?? {}); // ["year", "month", "day", "hour", "minute"]
|
|
2392
|
+
* ```
|
|
2393
|
+
*/
|
|
2394
|
+
interface DateParts {
|
|
2395
|
+
/** Calendar year as written, so `0050` is the year 50 and never 1950. */
|
|
2396
|
+
readonly year?: number;
|
|
2397
|
+
/** Calendar month, 1 to 12. */
|
|
2398
|
+
readonly month?: number;
|
|
2399
|
+
/** Day of month, 1 to 31. */
|
|
2400
|
+
readonly day?: number;
|
|
2401
|
+
/** Hour of day, 0 to 23. */
|
|
2402
|
+
readonly hour?: number;
|
|
2403
|
+
/** Minute, 0 to 59. */
|
|
2404
|
+
readonly minute?: number;
|
|
2405
|
+
/**
|
|
2406
|
+
* Second, 0 to 59.
|
|
2407
|
+
*
|
|
2408
|
+
* `TM` and `DT` permit `60` for a leap second and the decoders keep it, but a
|
|
2409
|
+
* value stating it is not projected: there is no ISO rendering of it a reader
|
|
2410
|
+
* does not move, and no instant to build. See the module doc block.
|
|
2411
|
+
*/
|
|
2412
|
+
readonly second?: number;
|
|
2413
|
+
/** The first three digits of the stated fraction, right-padded with zeroes. */
|
|
2414
|
+
readonly millisecond?: number;
|
|
2415
|
+
/** Signed minutes east of UTC, present only when the value carried an offset. */
|
|
2416
|
+
readonly offsetMinutes?: number;
|
|
2417
|
+
}
|
|
2418
|
+
/**
|
|
2419
|
+
* The one option {@link toDate} takes, and the only key it carries.
|
|
2420
|
+
*
|
|
2421
|
+
* `assumeOffsetMinutes` is the caller's declaration of the zone a value that
|
|
2422
|
+
* carries none was written in. An explicit `0` means "read this naive value as
|
|
2423
|
+
* UTC", which is a decision this library will not make on a caller's behalf.
|
|
2424
|
+
*
|
|
2425
|
+
* @example
|
|
2426
|
+
* ```ts
|
|
2427
|
+
* import { parseDate, toDate } from "@cosyte/dicom";
|
|
2428
|
+
* import type { ToDateOptions } from "@cosyte/dicom";
|
|
2429
|
+
*
|
|
2430
|
+
* const options: ToDateOptions = { assumeOffsetMinutes: -300 };
|
|
2431
|
+
* toDate(parseDate("20240115").value, options)?.toISOString(); // "2024-01-15T05:00:00.000Z"
|
|
2432
|
+
* ```
|
|
2433
|
+
*/
|
|
2434
|
+
interface ToDateOptions {
|
|
2435
|
+
/** Minutes east of UTC to read a value that states no offset in. */
|
|
2436
|
+
readonly assumeOffsetMinutes?: number;
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2439
|
+
* The calendar components a `DA`, `TM` or `DT` value stated, as a frozen object.
|
|
2440
|
+
*
|
|
2441
|
+
* Returns `undefined` for a value the decoders marked `valid: false`, for
|
|
2442
|
+
* `null` / `undefined`, for a value that stated no component at all, and for
|
|
2443
|
+
* one whose components name no real calendar point (30 February, 29 February
|
|
2444
|
+
* outside a leap year, 31 April, second 60). It never throws.
|
|
2445
|
+
* `hours` / `minutes` / `seconds` are renamed to the singular
|
|
2446
|
+
* `hour` / `minute` / `second`; `raw`, `valid` and the `legacy` and
|
|
2447
|
+
* `nonstandardOffset` flags the decoders report beside the value never appear.
|
|
2448
|
+
*
|
|
2449
|
+
* @example
|
|
2450
|
+
* ```ts
|
|
2451
|
+
* import { parseDate, parseDateTime, parseTime, toObject } from "@cosyte/dicom";
|
|
2452
|
+
*
|
|
2453
|
+
* toObject(parseDateTime("20240115133015").value);
|
|
2454
|
+
* // { year: 2024, month: 1, day: 15, hour: 13, minute: 30, second: 15 }
|
|
2455
|
+
*
|
|
2456
|
+
* toObject(parseTime("133015.123456").value).millisecond; // 123, from the digits
|
|
2457
|
+
* toObject(parseDate("18700230").value); // undefined: February has no 30th
|
|
2458
|
+
* ```
|
|
2459
|
+
*/
|
|
2460
|
+
declare function toObject(value: DicomTemporal | null | undefined): DateParts | undefined;
|
|
2461
|
+
/**
|
|
2462
|
+
* The value as an ISO-8601 string, TRUNCATED to the precision it stated.
|
|
2463
|
+
*
|
|
2464
|
+
* Nothing is padded out: a `DA` renders `2024-01-15`, a `DT` that stated only
|
|
2465
|
+
* an hour renders `2024-01-15T13`, and a `TM` renders the bare time. Fractional
|
|
2466
|
+
* digits are rendered exactly as written. A stated offset is appended, `Z` when
|
|
2467
|
+
* it is zero; a value that stated NO offset gets nothing appended, because a
|
|
2468
|
+
* fabricated `Z` would claim UTC the sender never wrote.
|
|
2469
|
+
*
|
|
2470
|
+
* Returns `undefined` for an invalid value, for `null` / `undefined`, for a
|
|
2471
|
+
* value that stated no component at all, and for one naming no real calendar
|
|
2472
|
+
* point: a string such as `"2024-02-30"` is worse than no answer, because every
|
|
2473
|
+
* ISO-8601 reader moves it silently to 1 March. It never throws. This is not a
|
|
2474
|
+
* byte round-trip of the wire value and is not meant to be: `serializeDicom`
|
|
2475
|
+
* remains the route that reproduces the original bytes.
|
|
2476
|
+
*
|
|
2477
|
+
* @example
|
|
2478
|
+
* ```ts
|
|
2479
|
+
* import { parseDate, parseDateTime, parseTime, toISO } from "@cosyte/dicom";
|
|
2480
|
+
*
|
|
2481
|
+
* toISO(parseDateTime("20240115133015-0500").value); // "2024-01-15T13:30:15-05:00"
|
|
2482
|
+
* toISO(parseTime("133015").value); // "13:30:15"
|
|
2483
|
+
* toISO(parseDate("18700431").value); // undefined: April has no 31st
|
|
2484
|
+
* ```
|
|
2485
|
+
*/
|
|
2486
|
+
declare function toISO(value: DicomTemporal | null | undefined): string | undefined;
|
|
2487
|
+
/**
|
|
2488
|
+
* The value as an absolute instant, ONLY where the zone is determinate.
|
|
2489
|
+
*
|
|
2490
|
+
* A stated `&ZZXX` offset wins and `options.assumeOffsetMinutes` is ignored.
|
|
2491
|
+
* With no stated offset the caller's `assumeOffsetMinutes` is applied, an
|
|
2492
|
+
* explicit `0` meaning "treat this naive value as UTC". With neither, the
|
|
2493
|
+
* answer is `undefined`: the host machine's zone is never read and UTC is never
|
|
2494
|
+
* assumed. A value with no year is never an instant, so a `TM` always answers
|
|
2495
|
+
* `undefined` however the call is made. A non-finite `assumeOffsetMinutes`
|
|
2496
|
+
* (`NaN`, `Infinity`, `-Infinity`) names no zone either, so it answers
|
|
2497
|
+
* `undefined` rather than an `Invalid Date` whose `getTime()` is `NaN`, and so
|
|
2498
|
+
* does a finite offset so large that applying it leaves the range a `Date` can
|
|
2499
|
+
* represent. A value naming no real calendar point is refused before any of
|
|
2500
|
+
* that, so an impossible day is never rolled into the next month.
|
|
2501
|
+
*
|
|
2502
|
+
* Components below the stated precision fill to their lowest legal value for
|
|
2503
|
+
* the instant alone; the value's own precision is untouched, and a later
|
|
2504
|
+
* {@link toObject} or {@link toISO} on it returns what it returned before. A
|
|
2505
|
+
* four-digit year below 100 stays that year, so `0050` is the year 50 and the
|
|
2506
|
+
* two-digit remapping of `Date.UTC` never reaches the result.
|
|
2507
|
+
*
|
|
2508
|
+
* @example
|
|
2509
|
+
* ```ts
|
|
2510
|
+
* import { parseDate, parseDateTime, toDate } from "@cosyte/dicom";
|
|
2511
|
+
*
|
|
2512
|
+
* toDate(parseDate("20240115").value); // undefined: no zone was stated
|
|
2513
|
+
* toDate(parseDate("20240115").value, { assumeOffsetMinutes: 0 })?.toISOString();
|
|
2514
|
+
* // "2024-01-15T00:00:00.000Z"
|
|
2515
|
+
* toDate(parseDateTime("20240115133015+0100").value)?.toISOString();
|
|
2516
|
+
* // "2024-01-15T12:30:15.000Z"
|
|
2517
|
+
* toDate(parseDate("18700230").value, { assumeOffsetMinutes: 0 });
|
|
2518
|
+
* // undefined: never 1 March for a value the sender wrote as February
|
|
2519
|
+
* ```
|
|
2520
|
+
*/
|
|
2521
|
+
declare function toDate(value: DicomTemporal | null | undefined, options?: ToDateOptions): Date | undefined;
|
|
2522
|
+
|
|
2523
|
+
/**
|
|
2524
|
+
* Coded-terminology surfacing (PS3.16 §8 Table 8-1).
|
|
1996
2525
|
*
|
|
1997
2526
|
* A coded concept is the triplet `Code Value (0008,0100)` /
|
|
1998
2527
|
* `Coding Scheme Designator (0008,0102)` / `Code Meaning (0008,0104)`. This
|
|
@@ -2051,11 +2580,11 @@ declare function codingSchemeOid(designator: string | undefined): string | undef
|
|
|
2051
2580
|
declare function readCode(item: Dataset): CodedConcept;
|
|
2052
2581
|
|
|
2053
2582
|
/**
|
|
2054
|
-
* Value-layer error taxonomy for the
|
|
2583
|
+
* Value-layer error taxonomy for the domain helpers.
|
|
2055
2584
|
*
|
|
2056
2585
|
* This is **separate** from the parser's four-code `FATAL_CODES` /
|
|
2057
2586
|
* `DicomParseError` (locked to unrecoverable Part 10 structural corruption
|
|
2058
|
-
* - see `../../parser/errors.ts`). The
|
|
2587
|
+
* - see `../../parser/errors.ts`). The domain helpers are otherwise fail-safe:
|
|
2059
2588
|
* a missing *value* is typed-absent (`undefined`), never an exception. The
|
|
2060
2589
|
* helper layer throws only for a **structural contract violation** the
|
|
2061
2590
|
* caller asked it to resolve and that cannot be answered safely:
|
|
@@ -2071,7 +2600,7 @@ declare function readCode(item: Dataset): CodedConcept;
|
|
|
2071
2600
|
* @module
|
|
2072
2601
|
*/
|
|
2073
2602
|
/**
|
|
2074
|
-
* Stable string codes the
|
|
2603
|
+
* Stable string codes the domain helpers may throw. Narrow on
|
|
2075
2604
|
* {@link DicomValueError.code} to react to a specific contract violation.
|
|
2076
2605
|
*
|
|
2077
2606
|
* @example
|
|
@@ -2103,7 +2632,7 @@ declare const VALUE_ERROR_CODES: {
|
|
|
2103
2632
|
*/
|
|
2104
2633
|
type ValueErrorCode = (typeof VALUE_ERROR_CODES)[keyof typeof VALUE_ERROR_CODES];
|
|
2105
2634
|
/**
|
|
2106
|
-
* Thrown by the
|
|
2635
|
+
* Thrown by the domain helpers for a structural contract violation that
|
|
2107
2636
|
* cannot be answered safely (see module doc). Never carries a decoded
|
|
2108
2637
|
* value, so it is safe to log without leaking PHI: the `message` is built
|
|
2109
2638
|
* only from the code and structural facts (indices, tag/macro names).
|
|
@@ -2134,7 +2663,7 @@ declare class DicomValueError extends Error {
|
|
|
2134
2663
|
}
|
|
2135
2664
|
|
|
2136
2665
|
/**
|
|
2137
|
-
* `serializeDicom` - the
|
|
2666
|
+
* `serializeDicom` - the Part 10 writer (the conservative half of
|
|
2138
2667
|
* Postel's Law).
|
|
2139
2668
|
*
|
|
2140
2669
|
* Takes a {@link Dataset} (from `parseDicom`, or hand-built) and emits a
|
|
@@ -2144,15 +2673,40 @@ declare class DicomValueError extends Error {
|
|
|
2144
2673
|
* 2. File Meta group `0002`, always Explicit VR LE, with a correct
|
|
2145
2674
|
* `(0002,0000)` group length - see {@link encodeFileMeta}.
|
|
2146
2675
|
* 3. The dataset body, in the dataset's own transfer syntax (**no
|
|
2147
|
-
* transcode**): Implicit VR LE
|
|
2148
|
-
* syntax
|
|
2149
|
-
* (`zlib.deflateRawSync`), symmetric to the parser's `inflateRawSync
|
|
2676
|
+
* transcode**): Implicit VR LE; Explicit VR LE/BE; for the Deflated
|
|
2677
|
+
* syntax, an Explicit VR LE body run through RFC 1951 raw deflate
|
|
2678
|
+
* (`zlib.deflateRawSync`), symmetric to the parser's `inflateRawSync`; and
|
|
2679
|
+
* for every Transfer Syntax PS3.5 2026c section A.4 names (the generated
|
|
2680
|
+
* list the parser dispatches), an Explicit VR LE body whose top-level
|
|
2681
|
+
* Pixel Data is an encapsulated fragment stream (see `./encapsulated.ts`).
|
|
2150
2682
|
*
|
|
2151
2683
|
* Conservative behaviour (PS3.5): scalar values are padded to even length on
|
|
2152
2684
|
* write (§6.2); retired `(gggg,0000)` group-length elements are omitted from
|
|
2153
|
-
* the dataset (§7.2); short/long-form headers are chosen by VR (§7.1.2);
|
|
2154
|
-
*
|
|
2155
|
-
* (§7.
|
|
2685
|
+
* the dataset (§7.2); short/long-form headers are chosen by VR (§7.1.2); the
|
|
2686
|
+
* Data Elements of the root Data Set, and of every Sequence Item the writer can
|
|
2687
|
+
* walk, are emitted in ascending tag order (PS3.5 2026c §7.1 / §7.5.1, see
|
|
2688
|
+
* `./order.ts`), with Items kept in their order and no value byte changed;
|
|
2689
|
+
* encapsulated-pixel-data fragments pass through byte-for-byte (§A.4), and
|
|
2690
|
+
* under a section A.4 syntax a top-level Pixel Data that is not a fragment
|
|
2691
|
+
* stream section A.4 allows is refused, never repaired. A DICOMDIR's Directory
|
|
2692
|
+
* Record offsets are recomputed against the bytes written, and one that cannot
|
|
2693
|
+
* be tied to a record is refused (see `./directory.ts`).
|
|
2694
|
+
*
|
|
2695
|
+
* The ordering has limits, stated with it. A tag repeated inside an Item is
|
|
2696
|
+
* kept (both copies, in source order), so such output still breaks PS3.5 2026c
|
|
2697
|
+
* §7.1's "at most once". A Sequence that cannot be walked, one nested past
|
|
2698
|
+
* `NESTING_DEPTH_LIMIT`, one whose parsed `items` do not match its bytes, and
|
|
2699
|
+
* any `UN`-carried Sequence (under Implicit VR LE that includes a private
|
|
2700
|
+
* Sequence inside an Item, even one a `Profile` resolved to `SQ`, since a
|
|
2701
|
+
* default read resolves its tag to `UN`) are emitted as read, unordered. An
|
|
2702
|
+
* element whose own bytes do not show where a reader ends it (an
|
|
2703
|
+
* undefined-length `UN` the parser could not read as a Sequence, a value
|
|
2704
|
+
* missing its Sequence Delimitation Item) is written after the ascending rest of
|
|
2705
|
+
* its Data Set, since a reader would take whatever followed it into its value.
|
|
2706
|
+
* So is a Sequence nested past the bound, other than a defined-length one under
|
|
2707
|
+
* Implicit VR LE, since seeing where a reader ends it would take the walk past
|
|
2708
|
+
* the bound. An element the parser relocated because a length lied is ordered
|
|
2709
|
+
* where it was placed.
|
|
2156
2710
|
*
|
|
2157
2711
|
* @module
|
|
2158
2712
|
*/
|
|
@@ -2161,9 +2715,59 @@ declare class DicomValueError extends Error {
|
|
|
2161
2715
|
* Serialize a {@link Dataset} to a spec-clean DICOM Part 10 `Buffer`.
|
|
2162
2716
|
*
|
|
2163
2717
|
* The dataset's transfer syntax is preserved (no transcoding): pixel-data
|
|
2164
|
-
* fragments
|
|
2165
|
-
*
|
|
2166
|
-
*
|
|
2718
|
+
* fragments are written back byte-for-byte, nested sequences carry the same
|
|
2719
|
+
* bytes with each Item's Data Elements in ascending tag order, and scalar values
|
|
2720
|
+
* are re-emitted with correct even-length padding and File Meta group length.
|
|
2721
|
+
* Pure function - the input `Dataset` is never mutated.
|
|
2722
|
+
*
|
|
2723
|
+
* **Element order.** Every Data Set is written in ascending tag order (PS3.5
|
|
2724
|
+
* 2026c §7.1, §7.5.1): the root, and the Data Set of every Item in every
|
|
2725
|
+
* Sequence the writer can walk on the wire, at every depth up to
|
|
2726
|
+
* `NESTING_DEPTH_LIMIT`. Items stay in their order (PS3.5 2026c §7.5) and no
|
|
2727
|
+
* value changes; only whole element spans move, and only where the parsed
|
|
2728
|
+
* `items` match the bytes, so the output reads back as the source did. Limits:
|
|
2729
|
+
* a tag repeated inside an Item is kept twice, in source order, so that output
|
|
2730
|
+
* still breaks PS3.5 2026c §7.1's "at most once"; a Sequence whose Item stream
|
|
2731
|
+
* cannot be walked to exactly its end, one nested past the bound, one whose
|
|
2732
|
+
* `items` do not match its bytes (a Sequence the parser did not descend, for
|
|
2733
|
+
* one), and any `UN`-carried Sequence (under Implicit VR LE that includes a
|
|
2734
|
+
* private Sequence inside an Item, even one a `Profile` resolved to `SQ`, since
|
|
2735
|
+
* a default read resolves its tag to `UN`) are written as read, unordered; an
|
|
2736
|
+
* element whose own bytes do not show where a reader ends it (an
|
|
2737
|
+
* undefined-length `UN` the parser could not read as a Sequence, or a value
|
|
2738
|
+
* missing its Sequence Delimitation Item) is written after the ascending rest
|
|
2739
|
+
* of its Data Set, because a reader takes what follows it into its value, and
|
|
2740
|
+
* so is a Sequence nested past the bound (other than a defined-length one under
|
|
2741
|
+
* Implicit VR LE), whose end the writer would have to walk past the bound to
|
|
2742
|
+
* see; and an element the parser relocated because a length lied is ordered
|
|
2743
|
+
* where it was placed, since ordering cannot recover an order the source
|
|
2744
|
+
* destroyed.
|
|
2745
|
+
*
|
|
2746
|
+
* **Encapsulated objects.** Under every Transfer Syntax PS3.5 2026c section A.4
|
|
2747
|
+
* names (JPEG, JPEG-LS, JPEG 2000, HTJ2K, RLE and the rest: the list `parseDicom`
|
|
2748
|
+
* reads), the File Meta Transfer Syntax UID is kept, the Data Set is written as
|
|
2749
|
+
* Explicit VR Little Endian, and the top-level Pixel Data `(7FE0,0010)` is
|
|
2750
|
+
* written as `OB` of undefined length: the input's Basic Offset Table Item and
|
|
2751
|
+
* fragment Items byte for byte and in order, then a zero-length Sequence
|
|
2752
|
+
* Delimitation Item. Nothing is decoded, transcoded or re-framed, and no offset
|
|
2753
|
+
* table is interpreted or rebuilt. The limit sits with it: a top-level Data Set
|
|
2754
|
+
* that section A.4 does not allow is refused with
|
|
2755
|
+
* `INVALID_ENCAPSULATED_PIXEL_DATA` rather than repaired, which includes a
|
|
2756
|
+
* fragment stream `parseDicom` read with `DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED`,
|
|
2757
|
+
* an odd Item Length, an empty fragment, native or absent top-level Pixel Data,
|
|
2758
|
+
* and Float or Double Float Pixel Data. Pixel Data nested in a Sequence Item is
|
|
2759
|
+
* written as read.
|
|
2760
|
+
*
|
|
2761
|
+
* **DICOMDIR offsets.** For a `Dataset` whose File Meta Media Storage SOP Class
|
|
2762
|
+
* UID is `1.2.840.10008.1.3.10`, `(0004,1200)`, `(0004,1202)` and each Directory
|
|
2763
|
+
* Record's `(0004,1400)` and `(0004,1420)` are written as the byte offset, from
|
|
2764
|
+
* the first preamble byte of the output, of the Directory Record each named when
|
|
2765
|
+
* the file was read (PS3.3 2026d Table F.3-3), whatever moved it: the rebuilt
|
|
2766
|
+
* File Meta group, the ascending order above, or `deidentify()`. A record is the
|
|
2767
|
+
* Item of the Directory Record Sequence `(0004,1220)` whose `Item.fileOffset`
|
|
2768
|
+
* the offset equals; nothing is found by scanning for an Item tag. A zero offset
|
|
2769
|
+
* is written as zero. Limits: the retired MRDR offset `(0004,1504)` is written as
|
|
2770
|
+
* read, and nothing checks record keys or `(0004,1202)` against the root chain.
|
|
2167
2771
|
*
|
|
2168
2772
|
* **Input contract.** The writer is designed for a {@link Dataset} produced by
|
|
2169
2773
|
* `parseDicom`: it relies on the parser's `Element.rawBytes` representation
|
|
@@ -2178,12 +2782,19 @@ declare class DicomValueError extends Error {
|
|
|
2178
2782
|
* elements preserved on `extraElements`), not a byte-exact copy of the original
|
|
2179
2783
|
* file: the 128-byte preamble is normalized to zeros, the File Meta group is
|
|
2180
2784
|
* rebuilt in ascending tag order (modeled fields + `extraElements` - see
|
|
2181
|
-
* {@link encodeFileMeta}),
|
|
2182
|
-
* `(gggg,0000)` group lengths are dropped.
|
|
2785
|
+
* {@link encodeFileMeta}), the Data Sets are ordered as above, odd-length values
|
|
2786
|
+
* are padded even, and retired `(gggg,0000)` group lengths are dropped.
|
|
2183
2787
|
*
|
|
2184
2788
|
* @throws {@link DicomSerializeError} with code `MISSING_TRANSFER_SYNTAX` when
|
|
2185
|
-
* the dataset has no File Meta Transfer Syntax UID,
|
|
2186
|
-
* `UNSUPPORTED_TRANSFER_SYNTAX` when that UID is
|
|
2789
|
+
* the dataset has no File Meta Transfer Syntax UID,
|
|
2790
|
+
* `UNSUPPORTED_TRANSFER_SYNTAX` when that UID is neither one of the four
|
|
2791
|
+
* native syntaxes nor a section A.4 one, `INVALID_ENCAPSULATED_PIXEL_DATA`
|
|
2792
|
+
* when it is a section A.4 one and the top-level Pixel Data is not a fragment
|
|
2793
|
+
* stream section A.4 allows, `DIRECTORY_OFFSET_UNRESOLVED` when a DICOMDIR
|
|
2794
|
+
* carries an offset that is not zero and names no Directory Record the
|
|
2795
|
+
* `Dataset` holds, or is not one 32-bit unsigned integer (never written stale
|
|
2796
|
+
* or as zero), or `DIRECTORY_OFFSET_DEFLATED` when a DICOMDIR under Deflated
|
|
2797
|
+
* Explicit VR LE carries a non-zero offset. Nothing is returned on a throw.
|
|
2187
2798
|
*
|
|
2188
2799
|
* @example
|
|
2189
2800
|
* ```ts
|
|
@@ -2196,9 +2807,9 @@ declare class DicomValueError extends Error {
|
|
|
2196
2807
|
declare function serializeDicom(ds: Dataset): Buffer;
|
|
2197
2808
|
|
|
2198
2809
|
/**
|
|
2199
|
-
* Serializer error taxonomy for the
|
|
2810
|
+
* Serializer error taxonomy for the Part 10 writer.
|
|
2200
2811
|
*
|
|
2201
|
-
* Like
|
|
2812
|
+
* Like {@link "../dataset/helpers/errors".DicomValueError}, this
|
|
2202
2813
|
* is **separate** from the parser's four-code `FATAL_CODES` /
|
|
2203
2814
|
* `DicomParseError` (which are locked to *read*-side Part 10 structural
|
|
2204
2815
|
* corruption - see `../parser/errors.ts`). The serializer throws only when
|
|
@@ -2208,17 +2819,45 @@ declare function serializeDicom(ds: Dataset): Buffer;
|
|
|
2208
2819
|
* `fileMeta.transferSyntaxUID` is empty. The Transfer Syntax UID is the
|
|
2209
2820
|
* dispatch input that decides every byte of the encoding, so there is no
|
|
2210
2821
|
* safe default to fall back to.
|
|
2211
|
-
* - `UNSUPPORTED_TRANSFER_SYNTAX` - the Transfer Syntax UID is
|
|
2212
|
-
* the four
|
|
2213
|
-
*
|
|
2214
|
-
*
|
|
2215
|
-
*
|
|
2216
|
-
*
|
|
2822
|
+
* - `UNSUPPORTED_TRANSFER_SYNTAX` - the Transfer Syntax UID is neither one of
|
|
2823
|
+
* the four native syntaxes nor one PS3.5 2026c section A.4 names for
|
|
2824
|
+
* encapsulated Pixel Data (the JPIP Referenced and SMPTE ST 2110 syntaxes
|
|
2825
|
+
* and every retired UID stay here). The writer never transcodes, so it
|
|
2826
|
+
* cannot emit a syntax it does not understand.
|
|
2827
|
+
* - `INVALID_ENCAPSULATED_PIXEL_DATA` - the Transfer Syntax UID is a section
|
|
2828
|
+
* A.4 one, but the top-level Data Set is not one that syntax may carry: no
|
|
2829
|
+
* top-level Pixel Data `(7FE0,0010)`, Float or Double Float Pixel Data
|
|
2830
|
+
* `(7FE0,0008)` / `(7FE0,0009)` present, Pixel Data with a defined Value
|
|
2831
|
+
* Length (Native Format), or a fragment stream that is not a Basic Offset
|
|
2832
|
+
* Table Item and one or more fragment Items of even, defined, non-zero
|
|
2833
|
+
* Item Length, ended by a Sequence Delimitation Item. The writer refuses
|
|
2834
|
+
* such a stream rather than repairing it: appending a delimiter to a stream
|
|
2835
|
+
* the reader flagged as possibly short would write a well-formed file that
|
|
2836
|
+
* may be missing frames.
|
|
2837
|
+
* - `DIRECTORY_OFFSET_UNRESOLVED` - the `Dataset` is a DICOMDIR (File Meta
|
|
2838
|
+
* Media Storage SOP Class UID `1.2.840.10008.1.3.10`) and one of its offset
|
|
2839
|
+
* attributes, `(0004,1200)`, `(0004,1202)`, or a Directory Record's
|
|
2840
|
+
* `(0004,1400)` or `(0004,1420)`, cannot be tied to a Directory Record the
|
|
2841
|
+
* `Dataset` holds: a non-zero value that is not the `Item.fileOffset` of an
|
|
2842
|
+
* Item of its Directory Record Sequence `(0004,1220)`, or a value that is
|
|
2843
|
+
* not one 32-bit unsigned integer. The writer recomputes every offset it
|
|
2844
|
+
* can tie; one it cannot is refused, never written stale (it would name the
|
|
2845
|
+
* wrong bytes) and never written as zero (zero means "no record" and would
|
|
2846
|
+
* silently prune the tree).
|
|
2847
|
+
* - `DIRECTORY_OFFSET_DEFLATED` - the DICOMDIR is to be written in Deflated
|
|
2848
|
+
* Explicit VR Little Endian and carries a non-zero offset (or one that is
|
|
2849
|
+
* not one 32-bit unsigned integer). A position inside a deflated stream
|
|
2850
|
+
* names no Item a reader can seek to, so no offset can be written for it.
|
|
2851
|
+
* A Deflated DICOMDIR whose offsets are all zero is written.
|
|
2852
|
+
*
|
|
2853
|
+
* The message is built only from structural constants (never a decoded
|
|
2854
|
+
* attribute value, a Transfer Syntax UID, or a length or byte read from the
|
|
2855
|
+
* input), so it is always safe to log.
|
|
2217
2856
|
*
|
|
2218
2857
|
* @module
|
|
2219
2858
|
*/
|
|
2220
2859
|
/**
|
|
2221
|
-
* Stable string codes the
|
|
2860
|
+
* Stable string codes the serializer may throw. Narrow on
|
|
2222
2861
|
* {@link DicomSerializeError.code} to react to a specific failure.
|
|
2223
2862
|
*
|
|
2224
2863
|
* @example
|
|
@@ -2230,6 +2869,9 @@ declare function serializeDicom(ds: Dataset): Buffer;
|
|
|
2230
2869
|
declare const SERIALIZE_ERROR_CODES: {
|
|
2231
2870
|
readonly MISSING_TRANSFER_SYNTAX: "MISSING_TRANSFER_SYNTAX";
|
|
2232
2871
|
readonly UNSUPPORTED_TRANSFER_SYNTAX: "UNSUPPORTED_TRANSFER_SYNTAX";
|
|
2872
|
+
readonly INVALID_ENCAPSULATED_PIXEL_DATA: "INVALID_ENCAPSULATED_PIXEL_DATA";
|
|
2873
|
+
readonly DIRECTORY_OFFSET_UNRESOLVED: "DIRECTORY_OFFSET_UNRESOLVED";
|
|
2874
|
+
readonly DIRECTORY_OFFSET_DEFLATED: "DIRECTORY_OFFSET_DEFLATED";
|
|
2233
2875
|
};
|
|
2234
2876
|
/**
|
|
2235
2877
|
* Discriminant for {@link DicomSerializeError.code}, enabling exhaustive
|
|
@@ -2243,7 +2885,13 @@ declare const SERIALIZE_ERROR_CODES: {
|
|
|
2243
2885
|
* case "MISSING_TRANSFER_SYNTAX":
|
|
2244
2886
|
* return "dataset has no Transfer Syntax UID to serialize under";
|
|
2245
2887
|
* case "UNSUPPORTED_TRANSFER_SYNTAX":
|
|
2246
|
-
* return "Transfer Syntax UID is
|
|
2888
|
+
* return "Transfer Syntax UID is neither native nor a section A.4 one";
|
|
2889
|
+
* case "INVALID_ENCAPSULATED_PIXEL_DATA":
|
|
2890
|
+
* return "top-level Pixel Data is not a section A.4 fragment stream";
|
|
2891
|
+
* case "DIRECTORY_OFFSET_UNRESOLVED":
|
|
2892
|
+
* return "a DICOMDIR offset names no Directory Record the dataset holds";
|
|
2893
|
+
* case "DIRECTORY_OFFSET_DEFLATED":
|
|
2894
|
+
* return "a Deflated DICOMDIR carries a non-zero Directory Record offset";
|
|
2247
2895
|
* }
|
|
2248
2896
|
* }
|
|
2249
2897
|
* ```
|
|
@@ -2253,7 +2901,8 @@ type SerializeErrorCode = (typeof SERIALIZE_ERROR_CODES)[keyof typeof SERIALIZE_
|
|
|
2253
2901
|
* Thrown by `serializeDicom` when a `Dataset`
|
|
2254
2902
|
* cannot be emitted as spec-clean Part 10. Never carries a decoded value, so
|
|
2255
2903
|
* it is safe to log without leaking PHI: the `message` is built only from the
|
|
2256
|
-
* code and the
|
|
2904
|
+
* code and structural constants, never from the Transfer Syntax UID, a length
|
|
2905
|
+
* or a byte the `Dataset` holds.
|
|
2257
2906
|
*
|
|
2258
2907
|
* @example
|
|
2259
2908
|
* ```ts
|
|
@@ -2265,6 +2914,12 @@ type SerializeErrorCode = (typeof SERIALIZE_ERROR_CODES)[keyof typeof SERIALIZE_
|
|
|
2265
2914
|
* if (err instanceof DicomSerializeError && err.code === "MISSING_TRANSFER_SYNTAX") {
|
|
2266
2915
|
* // dataset is missing the File Meta Transfer Syntax UID
|
|
2267
2916
|
* }
|
|
2917
|
+
* if (err instanceof DicomSerializeError && err.code === "INVALID_ENCAPSULATED_PIXEL_DATA") {
|
|
2918
|
+
* // a section A.4 object whose top-level Pixel Data is not a fragment stream
|
|
2919
|
+
* }
|
|
2920
|
+
* if (err instanceof DicomSerializeError && err.code === "DIRECTORY_OFFSET_UNRESOLVED") {
|
|
2921
|
+
* // a DICOMDIR offset the writer cannot tie to a Directory Record
|
|
2922
|
+
* }
|
|
2268
2923
|
* }
|
|
2269
2924
|
* ```
|
|
2270
2925
|
*/
|
|
@@ -2282,7 +2937,7 @@ declare class DicomSerializeError extends Error {
|
|
|
2282
2937
|
|
|
2283
2938
|
/**
|
|
2284
2939
|
* `defineProfile()` - the public factory for building immutable {@link Profile}
|
|
2285
|
-
* objects
|
|
2940
|
+
* objects. Mirrors the `@cosyte/hl7` sibling's profile factory: a
|
|
2286
2941
|
* validated options object in, a frozen `Profile` with a `describe()` method
|
|
2287
2942
|
* out. Invalid input throws {@link ProfileDefinitionError} with an actionable
|
|
2288
2943
|
* message rather than producing a half-built profile.
|
|
@@ -2425,22 +3080,24 @@ declare const profiles: Readonly<{
|
|
|
2425
3080
|
|
|
2426
3081
|
/**
|
|
2427
3082
|
* PS3.15 Annex E attribute-action table - DICOM Basic Application Confidentiality
|
|
2428
|
-
* Profile
|
|
2429
|
-
*
|
|
2430
|
-
*
|
|
2431
|
-
*
|
|
2432
|
-
*
|
|
2433
|
-
*
|
|
2434
|
-
*
|
|
2435
|
-
*
|
|
2436
|
-
*
|
|
2437
|
-
*
|
|
2438
|
-
*
|
|
2439
|
-
*
|
|
2440
|
-
*
|
|
2441
|
-
*
|
|
2442
|
-
*
|
|
2443
|
-
*
|
|
3083
|
+
* Profile plus its retention/clean option sets.
|
|
3084
|
+
*
|
|
3085
|
+
* Consumed by the de-identifier.
|
|
3086
|
+
*
|
|
3087
|
+
* NOTE: This module is NOT re-exported from the package's `src/index.ts`. The
|
|
3088
|
+
* `Dictionary` namespace's external surface is `Dictionary.{lookup,byKeyword,uid}`
|
|
3089
|
+
* plus `VERSION`; `annexE` is reached internally through
|
|
3090
|
+
* `@cosyte/dicom/dictionary/annex-e`.
|
|
3091
|
+
*
|
|
3092
|
+
* The 10 *metadata-affecting* option columns of Table E.1-1 populate
|
|
3093
|
+
* `AnnexEAction.optionSet` keys per attribute. Ten rather than nine because
|
|
3094
|
+
* E.3.6 is two mutually exclusive Options with a column each, and both are
|
|
3095
|
+
* carried: `RetainLongitudinalTemporal` for full dates and
|
|
3096
|
+
* `RetainLongitudinalTemporalModifiedDates` for modified dates. The two
|
|
3097
|
+
* *pixel-level* options - E.3.1 `CleanPixelData` and E.3.2
|
|
3098
|
+
* `CleanRecognizableVisual` - are not represented per-attribute (PS3.15 Table
|
|
3099
|
+
* E.1-1 has no column for them); they are enforced at the pixel-decode layer.
|
|
3100
|
+
* Both names remain in `AnnexEOption` for completeness and for that API.
|
|
2444
3101
|
*/
|
|
2445
3102
|
|
|
2446
3103
|
/**
|
|
@@ -2454,7 +3111,7 @@ declare const profiles: Readonly<{
|
|
|
2454
3111
|
* - `U` = replace UID with a consistent new UID per session
|
|
2455
3112
|
*
|
|
2456
3113
|
* Compound codes (`Z/D`, `X/Z/D`, `X/Z/U*`, `C/X`) are preserved verbatim from the
|
|
2457
|
-
* Annex E source;
|
|
3114
|
+
* Annex E source; the de-identifier interprets them per the table's per-attribute semantics
|
|
2458
3115
|
* (e.g. `Z/D` = `Z` if absent, `D` if present).
|
|
2459
3116
|
*
|
|
2460
3117
|
* @example
|
|
@@ -2462,52 +3119,68 @@ declare const profiles: Readonly<{
|
|
|
2462
3119
|
*/
|
|
2463
3120
|
type AnnexEActionCode = "D" | "Z" | "X" | "K" | "C" | "U" | "Z/D" | "X/Z" | "X/D" | "X/Z/D" | "X/Z/U*" | "C/X";
|
|
2464
3121
|
/**
|
|
2465
|
-
* One
|
|
3122
|
+
* One name per PS3.15 Annex E option column (E.3.1 to E.3.11), plus the second
|
|
3123
|
+
* of the two Options E.3.6 defines.
|
|
2466
3124
|
*
|
|
2467
|
-
*
|
|
2468
|
-
*
|
|
2469
|
-
*
|
|
2470
|
-
*
|
|
3125
|
+
* `CleanPixelData` (E.3.1) and `CleanRecognizableVisual` (E.3.2) act on pixel
|
|
3126
|
+
* data, not metadata, and never appear as `optionSet` keys in the generated
|
|
3127
|
+
* `ANNEX_E` map; they remain in the union for the pixel-decode API.
|
|
3128
|
+
*
|
|
3129
|
+
* **E.3.6 is two mutually exclusive Options and Table E.1-1 gives them separate
|
|
3130
|
+
* columns**, so it takes two names here: `RetainLongitudinalTemporal` carries
|
|
3131
|
+
* `Rtn. Long. Full Dates Opt.` (keep the real dates) and
|
|
3132
|
+
* `RetainLongitudinalTemporalModifiedDates` carries `Rtn. Long. Modif. Dates
|
|
3133
|
+
* Opt.` (keep them only as modified values), which is the more protective of
|
|
3134
|
+
* the two on every row where they differ.
|
|
2471
3135
|
*
|
|
2472
3136
|
* @example
|
|
2473
|
-
* const opt: AnnexEOption = "
|
|
3137
|
+
* const opt: AnnexEOption = "RetainLongitudinalTemporalModifiedDates";
|
|
2474
3138
|
*/
|
|
2475
|
-
type AnnexEOption = "CleanPixelData" | "CleanRecognizableVisual" | "CleanGraphics" | "CleanStructuredContent" | "CleanDescriptors" | "RetainLongitudinalTemporal" | "RetainPatientCharacteristics" | "RetainDeviceIdentity" | "RetainUIDs" | "RetainSafePrivate" | "RetainInstitutionIdentity";
|
|
3139
|
+
type AnnexEOption = "CleanPixelData" | "CleanRecognizableVisual" | "CleanGraphics" | "CleanStructuredContent" | "CleanDescriptors" | "RetainLongitudinalTemporal" | "RetainLongitudinalTemporalModifiedDates" | "RetainPatientCharacteristics" | "RetainDeviceIdentity" | "RetainUIDs" | "RetainSafePrivate" | "RetainInstitutionIdentity";
|
|
2476
3140
|
|
|
2477
3141
|
/**
|
|
2478
|
-
* Public types + error taxonomy for
|
|
3142
|
+
* Public types + error taxonomy for metadata de-identification
|
|
2479
3143
|
* (PS3.15 Annex E).
|
|
2480
3144
|
*
|
|
2481
3145
|
* @module
|
|
2482
3146
|
*/
|
|
2483
3147
|
|
|
2484
3148
|
/**
|
|
2485
|
-
* The PS3.15 Annex E option sets `deidentify` honours - the
|
|
3149
|
+
* The PS3.15 Annex E option sets `deidentify` honours - the
|
|
2486
3150
|
* *metadata-affecting* columns of Table E.1-1. The two pixel-level options
|
|
2487
3151
|
* (`CleanPixelData` §E.3.1, `CleanRecognizableVisual` §E.3.2) are deliberately
|
|
2488
3152
|
* excluded: this is a metadata-only de-identifier and cannot inspect pixels
|
|
2489
3153
|
* (deferred to `@cosyte/dicom-pixel`). When pixel data is present it always
|
|
2490
3154
|
* warns rather than claiming the image is clean.
|
|
2491
3155
|
*
|
|
2492
|
-
*
|
|
2493
|
-
*
|
|
2494
|
-
* `Rtn. Long.
|
|
2495
|
-
*
|
|
2496
|
-
*
|
|
2497
|
-
*
|
|
2498
|
-
*
|
|
2499
|
-
*
|
|
2500
|
-
*
|
|
2501
|
-
*
|
|
2502
|
-
*
|
|
2503
|
-
*
|
|
3156
|
+
* **§E.3.6 is two Options, and each has its own name here.** Table E.1-1 gives
|
|
3157
|
+
* them separate columns: `Rtn. Long. Full Dates` (keep dates and times as they
|
|
3158
|
+
* are) and `Rtn. Long. Modif. Dates` (keep them only as modified values).
|
|
3159
|
+
*
|
|
3160
|
+
* - `RetainLongitudinalTemporal` carries the **full-dates** column, the *less*
|
|
3161
|
+
* protective branch: where the two columns differ, it says `K` (keep the real
|
|
3162
|
+
* value). Activate it only when real dates are genuinely required.
|
|
3163
|
+
* - `RetainLongitudinalTemporalModifiedDates` carries the **modified-dates**
|
|
3164
|
+
* column, which says `C` (clean it) on every row where the two differ, and
|
|
3165
|
+
* makes the run write `(0028,0303) = MODIFIED`.
|
|
3166
|
+
*
|
|
3167
|
+
* The two are **mutually exclusive**: §E.3.6 defines them as alternatives, and a
|
|
3168
|
+
* call naming both is rejected with {@link DeidentifyError}. Leave both off and
|
|
3169
|
+
* the Basic Profile action applies, which removes or empties dates.
|
|
3170
|
+
*
|
|
3171
|
+
* 🩺 **Date *modification* is the caller's, on both branches.** This library
|
|
3172
|
+
* applies the column and writes the declaration; it shifts, aggregates and
|
|
3173
|
+
* transforms nothing, so a `MODIFIED` it writes is true only if you performed
|
|
3174
|
+
* the transformation §E.3.6 describes. `report.warnings` carries
|
|
3175
|
+
* `DICOM_DEIDENT_DATES_NOT_TRANSFORMED` on every run that activates the
|
|
3176
|
+
* modified-dates Option, saying exactly that.
|
|
2504
3177
|
*
|
|
2505
3178
|
* @example
|
|
2506
3179
|
* const retain: DeidentifyOption[] = ["RetainLongitudinalTemporal", "RetainSafePrivate"];
|
|
2507
3180
|
*/
|
|
2508
3181
|
type DeidentifyOption = Exclude<AnnexEOption, "CleanPixelData" | "CleanRecognizableVisual">;
|
|
2509
3182
|
/**
|
|
2510
|
-
* The
|
|
3183
|
+
* The metadata option-set names, frozen for runtime validation.
|
|
2511
3184
|
*
|
|
2512
3185
|
* @example
|
|
2513
3186
|
* ```ts
|
|
@@ -2594,8 +3267,16 @@ interface DeidentifiedAttribute {
|
|
|
2594
3267
|
* `(0012,0062) Patient Identity Removed = YES`. That re-emission belongs to
|
|
2595
3268
|
* the disclosed under-declare carrier class, not to this field, and neither is
|
|
2596
3269
|
* a bound on the other: **redacting `contextPath` from a log does not make the
|
|
2597
|
-
* object safe to share.**
|
|
2598
|
-
*
|
|
3270
|
+
* object safe to share.**
|
|
3271
|
+
*
|
|
3272
|
+
* **That fixture no longer reproduces either half.** `(5348,4E4F)` has no row
|
|
3273
|
+
* in this build's PS3.6 registry and none in Table E.1-1, so `deidentify()` now
|
|
3274
|
+
* removes the fabricated Sequence whole before it reads the VR, and neither
|
|
3275
|
+
* this field nor the object carries `"HSON"` (see
|
|
3276
|
+
* {@link UnregisteredElementRemoval}); that is what
|
|
3277
|
+
* `test/integration/phi-diagnostic-surface.test.ts` pins now. It is a fact
|
|
3278
|
+
* about that shape and **not a bound on this field**: a Sequence the run does
|
|
3279
|
+
* descend still contributes whatever tag the wire gave it.
|
|
2599
3280
|
*
|
|
2600
3281
|
* It is published anyway, on the same footing as
|
|
2601
3282
|
* {@link DeidentifyReport.removedPrivateTags}: **where** an attribute sat is
|
|
@@ -2633,7 +3314,7 @@ interface DeidentifiedAttribute {
|
|
|
2633
3314
|
* beside a genuine `(0010,0020)` reported `hidden: ["4D535449", "00100020"]`,
|
|
2634
3315
|
* and `4D535449` is `"SMIT"` in wire order.
|
|
2635
3316
|
*
|
|
2636
|
-
* **An entry is now one of the
|
|
3317
|
+
* **An entry is now one of the 653 literal rows of PS3.15 Table E.1-1 that this
|
|
2637
3318
|
* run's options left actionable.** That is a **membership** bound rather than a
|
|
2638
3319
|
* shape one - the posture this package already takes for a VR and for a Private
|
|
2639
3320
|
* Creator - so what survives names a published table entry rather than a
|
|
@@ -2686,11 +3367,12 @@ interface EmbeddedAttributeFinding {
|
|
|
2686
3367
|
readonly contextPath?: readonly string[];
|
|
2687
3368
|
}
|
|
2688
3369
|
/**
|
|
2689
|
-
* One
|
|
2690
|
-
*
|
|
3370
|
+
* One carrier this run could not reach the Data Sets inside, and **emptied** for
|
|
3371
|
+
* it. {@link UnauditableSequenceFinding.applied} names that outcome and is the
|
|
3372
|
+
* field to read first; it is the only outcome this finding has.
|
|
2691
3373
|
*
|
|
2692
|
-
* **Two producers, and
|
|
2693
|
-
* `SQ` element whose `items` the parser never materialized. The
|
|
3374
|
+
* **Two producers, and neither ships bytes.** The ordinary one is an
|
|
3375
|
+
* `SQ` element whose `items` the parser never materialized. The second is a
|
|
2694
3376
|
* private data element retained under `RetainSafePrivate` whose `Profile` entry
|
|
2695
3377
|
* declares it `SQ` while the parse tree says otherwise - `UN` under Implicit VR
|
|
2696
3378
|
* LE when the profile was passed to `deidentify()` but not to `parseDicom`, or
|
|
@@ -2701,6 +3383,19 @@ interface EmbeddedAttributeFinding {
|
|
|
2701
3383
|
* parsed VR in the output and is emptied rather than re-typed to `SQ`
|
|
2702
3384
|
* (`DICOM-PRIVATE-SQ-PARSE-VR`).
|
|
2703
3385
|
*
|
|
3386
|
+
* **🛑 THERE WAS A THIRD PRODUCER AND IT IS RETIRED: `applied: "kept"` IS GONE
|
|
3387
|
+
* FROM THIS TYPE AND FROM EVERY RUN.** Through `0.0.19` a private data element
|
|
3388
|
+
* the profile vouched for whose value this run never enumerated was **retained
|
|
3389
|
+
* verbatim** and named here with `applied: "kept"` - the package reporting that
|
|
3390
|
+
* it had shipped a value it did not read. Such an instance is now **removed**
|
|
3391
|
+
* instead, and its record moved to
|
|
3392
|
+
* {@link DeidentifyReport.unenumerablePrivateRemovals}, which is a different
|
|
3393
|
+
* surface with a different guarantee: uncapped, complete, and stating a reason.
|
|
3394
|
+
* This array never carries a retained private value again, so an entry here
|
|
3395
|
+
* always means content is **not** in your output. That is an audit-contract
|
|
3396
|
+
* change for anyone who switched on `applied`; see
|
|
3397
|
+
* {@link UnenumerablePrivateRemoval}.
|
|
3398
|
+
*
|
|
2704
3399
|
* PS3.5 2026c §7.5.1 "Item Encoding Rules" states that "Each Item Value shall
|
|
2705
3400
|
* contain a DICOM Data Set composed of Data Elements", so an `SQ` element's
|
|
2706
3401
|
* value is never opaque bytes - it is Data Elements this run is obliged to
|
|
@@ -2749,9 +3444,26 @@ interface EmbeddedAttributeFinding {
|
|
|
2749
3444
|
* ```
|
|
2750
3445
|
*/
|
|
2751
3446
|
interface UnauditableSequenceFinding {
|
|
2752
|
-
/** The
|
|
3447
|
+
/** The carrier element. */
|
|
2753
3448
|
readonly tag: Tag;
|
|
2754
|
-
/**
|
|
3449
|
+
/**
|
|
3450
|
+
* What happened to that carrier's value: `"emptied"`, always. The value is not
|
|
3451
|
+
* in the de-identified output.
|
|
3452
|
+
*
|
|
3453
|
+
* **🛑 `"kept"` WAS A MEMBER OF THIS UNION AND IS NOT ANY MORE.** It meant "the
|
|
3454
|
+
* value IS in the output, byte for byte, unexamined", and a report field that
|
|
3455
|
+
* can say so is exactly what this package no longer does: that instance is
|
|
3456
|
+
* removed now and recorded on
|
|
3457
|
+
* {@link DeidentifyReport.unenumerablePrivateRemovals}. The field itself is
|
|
3458
|
+
* kept, so `applied === "emptied"` keeps compiling and keeps meaning what it
|
|
3459
|
+
* always meant; a comparison against `"kept"` does not compile any more, which
|
|
3460
|
+
* is the intended way to find out.
|
|
3461
|
+
*/
|
|
3462
|
+
readonly applied: "emptied";
|
|
3463
|
+
/**
|
|
3464
|
+
* Byte length of the carrier's value field that was **dropped**. Structural,
|
|
3465
|
+
* never a decoded value.
|
|
3466
|
+
*/
|
|
2755
3467
|
readonly byteLength: number;
|
|
2756
3468
|
/**
|
|
2757
3469
|
* Tag/index chain when the carrier is inside a sequence item; omitted at the
|
|
@@ -2763,6 +3475,128 @@ interface UnauditableSequenceFinding {
|
|
|
2763
3475
|
*/
|
|
2764
3476
|
readonly contextPath?: readonly string[];
|
|
2765
3477
|
}
|
|
3478
|
+
/**
|
|
3479
|
+
* One private attribute **removed** because this run did not enumerate its
|
|
3480
|
+
* value - the record that discharges the audit half of the fail-safe, and the
|
|
3481
|
+
* one report surface that is complete at any input size.
|
|
3482
|
+
*
|
|
3483
|
+
* ## What "enumerated" means here, and what it deliberately does not
|
|
3484
|
+
*
|
|
3485
|
+
* A run has enumerated a private value when it **walked it as DICOM Data
|
|
3486
|
+
* Elements** and put each of them through the Annex E action table (a private
|
|
3487
|
+
* `SQ` the parser materialized items for), when the whole value **was matched
|
|
3488
|
+
* as a member of a closed table the caller supplied** (a Private Creator
|
|
3489
|
+
* `(gggg,00EE)` whose decoded value is in the profile's private dictionary), or
|
|
3490
|
+
* when the value is **zero-length** and so encodes no Data Set. Nothing else.
|
|
3491
|
+
*
|
|
3492
|
+
* **🛑 DECODING A VALUE UNDER THE VR THE PROFILE RESOLVED FOR IT IS NOT
|
|
3493
|
+
* ENUMERATION, AND NEITHER IS THE EMBEDDED-ATTRIBUTE SCANNER'S SILENCE.** A
|
|
3494
|
+
* decoded `LO`, `ST`, `OB`, `OW` or `UN` value is a byte run that can carry a
|
|
3495
|
+
* Data Set written in a transfer syntax this run never tested for; two cells of
|
|
3496
|
+
* the matrix in `test/integration/deident-private-reservation.test.ts` are
|
|
3497
|
+
* perfectly scannable Implicit VR LE string carriers whose values the scanner
|
|
3498
|
+
* DID read and found nothing in, and which carried a nested `(0010,0010)`
|
|
3499
|
+
* anyway, because the nested tiles are Explicit VR and the file is Implicit. So
|
|
3500
|
+
* the predicate is **what the run did with the value**, never the VR and never
|
|
3501
|
+
* the scanner's reach.
|
|
3502
|
+
*
|
|
3503
|
+
* ## The cost, stated at its real size
|
|
3504
|
+
*
|
|
3505
|
+
* With `RetainSafePrivate` plus a `Profile`, three classes of private value
|
|
3506
|
+
* reach the output on something this run enumerated: an instance the run walked
|
|
3507
|
+
* as Data Elements, a Private Creator the profile's dictionary vouches for, and
|
|
3508
|
+
* a zero-length value. **An ordinary vendor scalar under an ordinary string VR
|
|
3509
|
+
* that the file's own declaration does not name safe is removed**, because
|
|
3510
|
+
* this package enumerates nothing inside a retained private value that is not
|
|
3511
|
+
* one of those three. That is over-redaction traded for a closed identity leak:
|
|
3512
|
+
* PS3.15 2026c §E.3.10 retains Private Attributes "known by the de-identifier to
|
|
3513
|
+
* be safe from identity leakage" and sends "all other Private Attributes" to
|
|
3514
|
+
* removal or to the `(0008,0307)` element-specific action, which this library
|
|
3515
|
+
* does not implement - and a value nothing enumerated is not known to be safe
|
|
3516
|
+
* however ordinary it looks. Through `0.0.19` such a value was **kept** and
|
|
3517
|
+
* disclosed instead; the disclosure said outright that it was not a fix.
|
|
3518
|
+
*
|
|
3519
|
+
* ## 🩺 The fourth class, which this record never names
|
|
3520
|
+
*
|
|
3521
|
+
* A private value reaches the output on one more route, and it is the one that
|
|
3522
|
+
* rests on no enumeration at all: the **file's own** Private Data Element
|
|
3523
|
+
* Characteristics Sequence (0008,0300), which §E.3.10 names first among the ways
|
|
3524
|
+
* an Attribute is known safe and which needs no `Profile`. A value in a block
|
|
3525
|
+
* whose Block Identifying Information Status (0008,0303) reads `SAFE`, or listed
|
|
3526
|
+
* in a `MIXED` block's Nonidentifying Private Elements (0008,0304), is retained
|
|
3527
|
+
* unexamined on the **sender's** written assertion, so it is never removed and
|
|
3528
|
+
* never appears here. That is the opposite trade from the one above, made in the
|
|
3529
|
+
* open: the caller who does not trust the sender leaves `RetainSafePrivate` off,
|
|
3530
|
+
* which is the one mitigation §E.3.10 offers.
|
|
3531
|
+
*
|
|
3532
|
+
* ## Per INSTANCE, never per tag
|
|
3533
|
+
*
|
|
3534
|
+
* An entry names one **occurrence**: a tag together with the Data Set it lived
|
|
3535
|
+
* in, which `contextPath` identifies (absent means the root Data Set). Private
|
|
3536
|
+
* blocks are reserved per Data Set (PS3.5 §7.8.1), so the same private tag can
|
|
3537
|
+
* occur several times in one object with different enumerability, and removing
|
|
3538
|
+
* one occurrence never removes a sibling one.
|
|
3539
|
+
*
|
|
3540
|
+
* ## Uncapped, and that is a decision rather than an oversight
|
|
3541
|
+
*
|
|
3542
|
+
* Every consumer-controlled **diagnostic** in this package is capped, because a
|
|
3543
|
+
* finding emitted per element is amplified by an element count the input
|
|
3544
|
+
* chooses. This is not a diagnostic: it is the record of an **action**, on the
|
|
3545
|
+
* same footing as {@link DeidentifyReport.removedPrivateTags}, which is uncapped
|
|
3546
|
+
* for the same reason. A caller's whole guarantee is that they can separate the
|
|
3547
|
+
* unenumerable removals from the Annex E ones for **every** removal, so a cap
|
|
3548
|
+
* here would silently take the guarantee away exactly on the files that need it
|
|
3549
|
+
* most. The matching warnings stay bounded; this array does not.
|
|
3550
|
+
*
|
|
3551
|
+
* ## What it carries, and what that is worth logging
|
|
3552
|
+
*
|
|
3553
|
+
* `tag` is composed from four bytes of the source. On a well-formed file it is
|
|
3554
|
+
* the sender's own private tag number and carries nothing, but on a file whose
|
|
3555
|
+
* Value Lengths disagree with its bytes those four bytes can be document
|
|
3556
|
+
* content - the standing exception {@link DeidentifyReport.removedPrivateTags}
|
|
3557
|
+
* and {@link UnauditableSequenceFinding} already carry, by the same route, and
|
|
3558
|
+
* `contextPath` is unbound in the same way. It is published anyway, because
|
|
3559
|
+
* *which* attribute was removed and *where* is the whole audit value of the
|
|
3560
|
+
* record. Treat this array as PHI when the source is untrusted.
|
|
3561
|
+
*
|
|
3562
|
+
* @example
|
|
3563
|
+
* ```ts
|
|
3564
|
+
* import { deidentify, parseDicom, type UnenumerablePrivateRemoval } from "@cosyte/dicom";
|
|
3565
|
+
* const { report } = deidentify(parseDicom(buf), { retain: ["RetainSafePrivate"], profile });
|
|
3566
|
+
* report.unenumerablePrivateRemovals.forEach((r: UnenumerablePrivateRemoval) => {
|
|
3567
|
+
* console.warn(`${r.tag} ${r.applied} (${r.reason})`, r.contextPath ?? "root");
|
|
3568
|
+
* });
|
|
3569
|
+
* ```
|
|
3570
|
+
*/
|
|
3571
|
+
interface UnenumerablePrivateRemoval {
|
|
3572
|
+
/** The removed instance's tag. */
|
|
3573
|
+
readonly tag: Tag;
|
|
3574
|
+
/**
|
|
3575
|
+
* The outcome, always `"removed"`: the Data Set that held this instance
|
|
3576
|
+
* carries no element bearing that tag, in the returned dataset and in the
|
|
3577
|
+
* serialized bytes. Distinct from **emptied**, where an element with that tag
|
|
3578
|
+
* is present carrying a zero-length value.
|
|
3579
|
+
*/
|
|
3580
|
+
readonly applied: "removed";
|
|
3581
|
+
/**
|
|
3582
|
+
* Why, always `"unenumerable"`: this run did not enumerate the value, so
|
|
3583
|
+
* PS3.15 §E.3.10's "known ... to be safe" was never established for it. This
|
|
3584
|
+
* is what separates an entry here from an attribute the Annex E action table
|
|
3585
|
+
* removed (`report.attributes`, `applied: "removed"`) and from one whose value
|
|
3586
|
+
* was emptied.
|
|
3587
|
+
*/
|
|
3588
|
+
readonly reason: "unenumerable";
|
|
3589
|
+
/**
|
|
3590
|
+
* Tag/index chain naming the Data Set this instance lived in; **omitted at the
|
|
3591
|
+
* root**, which is what identifies the removal as per-instance rather than
|
|
3592
|
+
* per-tag. **Built by the same descent as
|
|
3593
|
+
* {@link DeidentifiedAttribute.contextPath} and carrying the same caveat:
|
|
3594
|
+
* each segment's tag is read off the wire, bound by nothing, so on a
|
|
3595
|
+
* desynchronized read it can be four bytes of a value.** Read that field's
|
|
3596
|
+
* note before logging this one.
|
|
3597
|
+
*/
|
|
3598
|
+
readonly contextPath?: readonly string[];
|
|
3599
|
+
}
|
|
2766
3600
|
/**
|
|
2767
3601
|
* One element that was emptied because its **on-wire VR is not one of the 34
|
|
2768
3602
|
* PS3.5 section 6.2 defines**, so nothing this library did to its bytes counts
|
|
@@ -2877,6 +3711,214 @@ interface UndefinedVrFinding {
|
|
|
2877
3711
|
*/
|
|
2878
3712
|
readonly contextPath?: readonly string[];
|
|
2879
3713
|
}
|
|
3714
|
+
/**
|
|
3715
|
+
* One non-modeled `(0002,xxxx)` File Meta element `deidentify()` **dropped**
|
|
3716
|
+
* rather than re-emitting into de-identified output.
|
|
3717
|
+
*
|
|
3718
|
+
* ## Why the drop is unconditional
|
|
3719
|
+
*
|
|
3720
|
+
* PS3.15 2026c §E.1.1 requires the File Meta Information of a de-identified
|
|
3721
|
+
* DICOM File to "be replaced with a description of the de-identifying
|
|
3722
|
+
* application", because otherwise "identity information may leak through
|
|
3723
|
+
* unmodified File Meta Information ... [t]his includes information regarding
|
|
3724
|
+
* Application Entity Titles, Presentation Addresses, implementation
|
|
3725
|
+
* information, and private information". A non-modeled `(0002,xxxx)` element is
|
|
3726
|
+
* by definition one this library cannot describe: `(0002,0017)` Sending AE
|
|
3727
|
+
* Title, `(0002,0018)` Receiving AE Title, `(0002,0100)` Private Information
|
|
3728
|
+
* Creator UID and `(0002,0102)` Private Information are all in that class, and
|
|
3729
|
+
* so is anything a vendor invented. The fail-safe direction is to drop what
|
|
3730
|
+
* cannot be described, so no Annex E Option and no parse quality changes the
|
|
3731
|
+
* outcome.
|
|
3732
|
+
*
|
|
3733
|
+
* ## Capped record, complete count
|
|
3734
|
+
*
|
|
3735
|
+
* The array is bounded per run (`MAX_FILE_META_DROP_FINDINGS`) because the
|
|
3736
|
+
* number of elements a crafted File Meta group can carry is chosen by the input.
|
|
3737
|
+
* The **action** is not bounded: every such element is dropped whether or not it
|
|
3738
|
+
* is listed, and {@link DeidentifyReport.fileMetaElementsDroppedCount} is the
|
|
3739
|
+
* complete total at any input size. An array exactly the cap's length means "at
|
|
3740
|
+
* least this many"; read the count, not the length.
|
|
3741
|
+
*
|
|
3742
|
+
* ## What it carries
|
|
3743
|
+
*
|
|
3744
|
+
* `tag` and `vr` come off the wire. The File Meta pre-pass stops at the first
|
|
3745
|
+
* element whose group is not `0002`, so a tag reaching this record always has
|
|
3746
|
+
* `0002` as its group, but its element number is two bytes the file supplied and
|
|
3747
|
+
* a File Meta element that over-declared its length can desynchronize the
|
|
3748
|
+
* pre-pass onto two bytes of somebody's value. Same standing exception as
|
|
3749
|
+
* {@link DeidentifyReport.removedPrivateTags}, by a narrower route and with two
|
|
3750
|
+
* bytes rather than four. `byteLength` is the length of the value that was
|
|
3751
|
+
* dropped, read off that same header. It is published because *what* was dropped
|
|
3752
|
+
* is the whole audit value of a record of a deliberate fidelity loss.
|
|
3753
|
+
*
|
|
3754
|
+
* @example
|
|
3755
|
+
* ```ts
|
|
3756
|
+
* import { deidentify, parseDicom, type FileMetaDroppedElement } from "@cosyte/dicom";
|
|
3757
|
+
* const { report } = deidentify(parseDicom(buf));
|
|
3758
|
+
* report.fileMetaElementsDropped.forEach((d: FileMetaDroppedElement) => {
|
|
3759
|
+
* console.warn(`${d.tag} ${d.vr}: ${String(d.byteLength)} bytes not re-emitted`);
|
|
3760
|
+
* });
|
|
3761
|
+
* ```
|
|
3762
|
+
*/
|
|
3763
|
+
interface FileMetaDroppedElement {
|
|
3764
|
+
/** The dropped element's `(0002,xxxx)` tag. */
|
|
3765
|
+
readonly tag: Tag;
|
|
3766
|
+
/** The VR the source file wrote for it (the File Meta group is Explicit VR LE). */
|
|
3767
|
+
readonly vr: VR;
|
|
3768
|
+
/** Byte length of the value that is not in the de-identified output. */
|
|
3769
|
+
readonly byteLength: number;
|
|
3770
|
+
}
|
|
3771
|
+
/**
|
|
3772
|
+
* One Data Element whose Group Number is `0004`, **removed** from de-identified
|
|
3773
|
+
* output.
|
|
3774
|
+
*
|
|
3775
|
+
* ## The rule, and the one object it does not apply to
|
|
3776
|
+
*
|
|
3777
|
+
* PS3.15 2026c §E.1.1: "All Data Elements with a Group Number of 0004 shall be
|
|
3778
|
+
* removed from any SOP Instance or DICOM File other than a DICOMDIR File." The
|
|
3779
|
+
* rule is unconditional over the Annex E Options - it sits in the list §E.1.1
|
|
3780
|
+
* imposes on any de-identifier claim and no Option qualifies it - so no
|
|
3781
|
+
* `retain` argument brings a `(0004,xxxx)` element back.
|
|
3782
|
+
*
|
|
3783
|
+
* The DICOMDIR carve-out is decided once per run from the source File Meta's
|
|
3784
|
+
* `(0002,0002)` Media Storage SOP Class UID, and only the value
|
|
3785
|
+
* `1.2.840.10008.1.3.10` (Media Storage Directory Storage) selects it. An object
|
|
3786
|
+
* that declares no Media Storage SOP Class UID at all is therefore **not** a
|
|
3787
|
+
* DICOMDIR and the removal applies to it. When the carve-out does fire, this
|
|
3788
|
+
* array is empty and `DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED` says what
|
|
3789
|
+
* else the run did not do.
|
|
3790
|
+
*
|
|
3791
|
+
* ## Every depth, and the removal decides nothing below it
|
|
3792
|
+
*
|
|
3793
|
+
* The test runs on each element of each Data Set the run reaches, root and
|
|
3794
|
+
* Sequence Item alike, so a `(0004,xxxx)` planted inside an item is removed
|
|
3795
|
+
* there too. It is a test on **one element's tag**: it never stands in for a
|
|
3796
|
+
* decision about the Data Sets nested inside some other element, which is the
|
|
3797
|
+
* shape `DICOM-PRIVATE-SQ-CARVE-OUT` was opened for. Removing a `(0004,xxxx)`
|
|
3798
|
+
* that is itself a Sequence removes its items with it, exactly as a Table E.1-1
|
|
3799
|
+
* `X` action does.
|
|
3800
|
+
*
|
|
3801
|
+
* ## Capped record, complete count
|
|
3802
|
+
*
|
|
3803
|
+
* Bounded per run at `MAX_GROUP_0004_FINDINGS`, on its own counter so a flood of
|
|
3804
|
+
* one diagnostic class cannot spend another's budget. The removal itself is not
|
|
3805
|
+
* bounded, and {@link DeidentifyReport.group0004RemovalCount} is complete at any
|
|
3806
|
+
* input size.
|
|
3807
|
+
*
|
|
3808
|
+
* ## What it carries
|
|
3809
|
+
*
|
|
3810
|
+
* `tag` is composed from four bytes of the source and `contextPath` from four
|
|
3811
|
+
* more per segment - the standing exception
|
|
3812
|
+
* {@link DeidentifyReport.removedPrivateTags} and
|
|
3813
|
+
* {@link UnauditableSequenceFinding} already carry, by the same route. On a
|
|
3814
|
+
* well-formed file these are the DICOMDIR directory-structuring tags PS3.6
|
|
3815
|
+
* registers and carry nothing.
|
|
3816
|
+
*
|
|
3817
|
+
* @example
|
|
3818
|
+
* ```ts
|
|
3819
|
+
* import { deidentify, parseDicom, type Group0004Removal } from "@cosyte/dicom";
|
|
3820
|
+
* const { report } = deidentify(parseDicom(buf));
|
|
3821
|
+
* report.group0004Removals.forEach((r: Group0004Removal) => {
|
|
3822
|
+
* console.warn(`${r.tag} removed`, r.contextPath ?? "root");
|
|
3823
|
+
* });
|
|
3824
|
+
* ```
|
|
3825
|
+
*/
|
|
3826
|
+
interface Group0004Removal {
|
|
3827
|
+
/** The removed element's tag. Its group is always `0004`. */
|
|
3828
|
+
readonly tag: Tag;
|
|
3829
|
+
/**
|
|
3830
|
+
* The outcome, always `"removed"`: no element bearing that tag is in the Data
|
|
3831
|
+
* Set that held it, in the returned dataset or in the serialized bytes.
|
|
3832
|
+
*/
|
|
3833
|
+
readonly applied: "removed";
|
|
3834
|
+
/**
|
|
3835
|
+
* Tag/index chain when the element was inside a sequence item; omitted at the
|
|
3836
|
+
* root. **Built by the same descent as
|
|
3837
|
+
* {@link DeidentifiedAttribute.contextPath} and carrying the same caveat:
|
|
3838
|
+
* each segment's tag is read off the wire, bound by nothing, so on a
|
|
3839
|
+
* desynchronized read it can be four bytes of a value.** Read that field's
|
|
3840
|
+
* note before logging this one.
|
|
3841
|
+
*/
|
|
3842
|
+
readonly contextPath?: readonly string[];
|
|
3843
|
+
}
|
|
3844
|
+
/**
|
|
3845
|
+
* One non-private Data Element **removed** because this build's PS3.6 2026d
|
|
3846
|
+
* registry does not carry its tag and PS3.15 2026d Table E.1-1 does not list it.
|
|
3847
|
+
*
|
|
3848
|
+
* ## The rule
|
|
3849
|
+
*
|
|
3850
|
+
* A Table E.1-1 miss used to mean "keep": an attribute the table does not list
|
|
3851
|
+
* went into de-identified output verbatim with a report that said nothing about
|
|
3852
|
+
* it. That is right for an attribute PS3.6 registers, which the Profile has
|
|
3853
|
+
* judged, and wrong for one it does not: the notes to PS3.15 2026d Table E.1-1
|
|
3854
|
+
* name "new Standard Attributes" among the places identifying information may
|
|
3855
|
+
* be, and say removing only the known risks "may fail when the Standard is
|
|
3856
|
+
* extended, or when a vendor adds unanticipated Standard Attributes". So an
|
|
3857
|
+
* even-group tag with **no registry row and no Table E.1-1 row** is removed, at
|
|
3858
|
+
* every depth `deidentify()` walks, whatever its VR (`UN`, an on-wire VR outside
|
|
3859
|
+
* the 34 PS3.5 section 6.2 defines, and `SQ` included; a Sequence goes whole and
|
|
3860
|
+
* nothing inside it is walked or recorded).
|
|
3861
|
+
*
|
|
3862
|
+
* "Registered" means a literal PS3.6 row, or a masked row the tag matches, with
|
|
3863
|
+
* the `50xx` / `60xx` groups bounded by PS3.5 2026c section 7.6 (even groups
|
|
3864
|
+
* `5000`-`501E` and `6000`-`601E`). Private tags, group `0004`, group `0002`,
|
|
3865
|
+
* group lengths `(gggg,0000)` and every tag Table E.1-1 lists are not decided by
|
|
3866
|
+
* this rule.
|
|
3867
|
+
*
|
|
3868
|
+
* ## What it costs
|
|
3869
|
+
*
|
|
3870
|
+
* **A conformant attribute from a PS3.6 edition newer than this build's pin is
|
|
3871
|
+
* removed too**, because nothing separates it from an invented one. That is
|
|
3872
|
+
* over-redaction and it is deliberate; a caller who needs such an attribute back
|
|
3873
|
+
* has no switch for it in this release.
|
|
3874
|
+
*
|
|
3875
|
+
* ## What it carries, and what it deliberately does not
|
|
3876
|
+
*
|
|
3877
|
+
* **No tag and no VR.** The trigger is "no registry row carries this tag", and
|
|
3878
|
+
* four bytes an under-declared length upstream made the reader take as a header
|
|
3879
|
+
* satisfy it by construction: the fixture in
|
|
3880
|
+
* `test/integration/deident-undefined-vr.test.ts` fabricates `(4854,4F53)`,
|
|
3881
|
+
* `"THSO"` in wire order, four letters of a surname. Publishing the tag would
|
|
3882
|
+
* republish them, so the element is identified by `byteOffset`, a position this
|
|
3883
|
+
* parser counted, exactly as {@link UndefinedVrFinding} does.
|
|
3884
|
+
*
|
|
3885
|
+
* `contextPath` names the Sequences this run **descended** to reach the
|
|
3886
|
+
* element, never the removed element itself, and carries the caveat every
|
|
3887
|
+
* finding's `contextPath` carries: see {@link DeidentifiedAttribute.contextPath}.
|
|
3888
|
+
*
|
|
3889
|
+
* ## Capped record, complete count
|
|
3890
|
+
*
|
|
3891
|
+
* Bounded per run at `MAX_UNREGISTERED_ELEMENT_FINDINGS`, on its own counter so
|
|
3892
|
+
* a flood of one diagnostic class cannot spend another's budget. The removal
|
|
3893
|
+
* itself is not bounded, and
|
|
3894
|
+
* {@link DeidentifyReport.unregisteredElementRemovalCount} is complete at any
|
|
3895
|
+
* input size.
|
|
3896
|
+
*
|
|
3897
|
+
* @example
|
|
3898
|
+
* ```ts
|
|
3899
|
+
* import { deidentify, parseDicom, type UnregisteredElementRemoval } from "@cosyte/dicom";
|
|
3900
|
+
* const { report } = deidentify(parseDicom(buf));
|
|
3901
|
+
* report.unregisteredElementRemovals.forEach((r: UnregisteredElementRemoval) => {
|
|
3902
|
+
* console.warn(`removed at offset ${String(r.byteOffset)}`, r.contextPath ?? "root");
|
|
3903
|
+
* });
|
|
3904
|
+
* ```
|
|
3905
|
+
*/
|
|
3906
|
+
interface UnregisteredElementRemoval {
|
|
3907
|
+
/**
|
|
3908
|
+
* Byte offset of the removed element's header, as the parser recorded it on
|
|
3909
|
+
* `Element.byteOffset`. **This is how the element is identified, and there is
|
|
3910
|
+
* deliberately no `tag` field**: see the note above.
|
|
3911
|
+
*/
|
|
3912
|
+
readonly byteOffset: number;
|
|
3913
|
+
/**
|
|
3914
|
+
* Tag/index chain when the element was inside a sequence item; omitted at the
|
|
3915
|
+
* root. **Built by the same descent as
|
|
3916
|
+
* {@link DeidentifiedAttribute.contextPath} and carrying the same caveat:
|
|
3917
|
+
* each segment's tag is read off the wire.** Read that field's note before
|
|
3918
|
+
* logging this one.
|
|
3919
|
+
*/
|
|
3920
|
+
readonly contextPath?: readonly string[];
|
|
3921
|
+
}
|
|
2880
3922
|
/**
|
|
2881
3923
|
* The audit trail returned alongside the de-identified dataset.
|
|
2882
3924
|
*
|
|
@@ -2900,13 +3942,18 @@ interface UndefinedVrFinding {
|
|
|
2900
3942
|
* theoretical.
|
|
2901
3943
|
* - **`unauditableSequences[].tag`** - see
|
|
2902
3944
|
* {@link UnauditableSequenceFinding}. Same shape as `removedPrivateTags` by a
|
|
2903
|
-
* narrower route: a private carrier a {@link Profile}
|
|
2904
|
-
* there, and an under-declared length upstream can
|
|
2905
|
-
* onto four bytes that spell such a block. The
|
|
3945
|
+
* narrower route: a private carrier a {@link Profile} vouched for and this run
|
|
3946
|
+
* emptied is named there, and an under-declared length upstream can
|
|
3947
|
+
* resynchronize the reader onto four bytes that spell such a block. The
|
|
3948
|
+
* package's usual answer to a
|
|
2906
3949
|
* fabricated header, `undefinedVrElements`, carries a byte offset and no tag,
|
|
2907
3950
|
* and still answers it whenever the fabricated VR is outside the 34 PS3.5
|
|
2908
3951
|
* §6.2 defines. It cannot when the fabricated VR is one of them, because those
|
|
2909
3952
|
* two files are byte-identical.
|
|
3953
|
+
* - **`unenumerablePrivateRemovals[].tag`** - see
|
|
3954
|
+
* {@link UnenumerablePrivateRemoval}. The same four bytes by the same route,
|
|
3955
|
+
* on the record of the removal rather than of an emptying, and this one is
|
|
3956
|
+
* **uncapped** because it is the record of an action rather than a diagnostic.
|
|
2910
3957
|
* - **`undefinedVrElements[].byteLength` and
|
|
2911
3958
|
* `unauditableSequences[].byteLength`** - the declared Value Length read off
|
|
2912
3959
|
* the element header, so on a fabricated header it is four document bytes
|
|
@@ -2919,7 +3966,17 @@ interface UndefinedVrFinding {
|
|
|
2919
3966
|
* them is a product call rather than a defect fix: a bound empties the field
|
|
2920
3967
|
* on every well-formed file, where the number is exactly the audit
|
|
2921
3968
|
* information it exists to carry.
|
|
2922
|
-
* - **`
|
|
3969
|
+
* - **`group0004Removals[].tag`** - see {@link Group0004Removal}. The same four
|
|
3970
|
+
* bytes by the same route as `removedPrivateTags`, on the record of a removal
|
|
3971
|
+
* PS3.15 §E.1.1 requires. On a well-formed file these are PS3.6's registered
|
|
3972
|
+
* DICOMDIR directory-structuring tags and carry nothing.
|
|
3973
|
+
* - **`fileMetaElementsDropped[].tag` and `.vr`** - see
|
|
3974
|
+
* {@link FileMetaDroppedElement}. Two bytes rather than four: the File Meta
|
|
3975
|
+
* pre-pass stops at the first non-`0002` group, so the group half of the tag
|
|
3976
|
+
* is fixed and only the element number is free, and the VR is two more bytes
|
|
3977
|
+
* the same desynchronized read would supply. `.byteLength` joins
|
|
3978
|
+
* `undefinedVrElements[].byteLength` in the entry above by the same route.
|
|
3979
|
+
* - **`contextPath`, on every finding that carries one** - see
|
|
2923
3980
|
* {@link DeidentifiedAttribute.contextPath}, which holds the measurement. The
|
|
2924
3981
|
* segment tags come off the wire with no table behind them, so a fabricated
|
|
2925
3982
|
* `SQ` header the reader descended is named there, `PRE-EXISTING`, with **no
|
|
@@ -2954,7 +4011,14 @@ interface DeidentifyReport {
|
|
|
2954
4011
|
/** Per-attribute outcomes for every attribute Annex E acted on. */
|
|
2955
4012
|
readonly attributes: readonly DeidentifiedAttribute[];
|
|
2956
4013
|
/**
|
|
2957
|
-
* Private tags removed
|
|
4014
|
+
* Private tags removed (kept ones are omitted) - **every one of them,
|
|
4015
|
+
* whichever rule removed it**: the Basic Profile's default removal, a
|
|
4016
|
+
* reservation the file did not settle, and, since the unenumerable class
|
|
4017
|
+
* became a removal, a private attribute this run did not enumerate. Its
|
|
4018
|
+
* meaning is unchanged and it is still uncapped; what it does not carry is a
|
|
4019
|
+
* **reason**, which is why the unenumerable removals are also recorded on
|
|
4020
|
+
* {@link DeidentifyReport.unenumerablePrivateRemovals}, where they can be told
|
|
4021
|
+
* apart from the rest.
|
|
2958
4022
|
*
|
|
2959
4023
|
* **This is the second field that is not value-free, and the qualification is
|
|
2960
4024
|
* measured.** A tag here is composed from four bytes of the source, and on a
|
|
@@ -2981,18 +4045,33 @@ interface DeidentifyReport {
|
|
|
2981
4045
|
*/
|
|
2982
4046
|
readonly embeddedAttributes: readonly EmbeddedAttributeFinding[];
|
|
2983
4047
|
/**
|
|
2984
|
-
*
|
|
2985
|
-
*
|
|
2986
|
-
*
|
|
2987
|
-
*
|
|
2988
|
-
*
|
|
2989
|
-
*
|
|
4048
|
+
* Carriers whose nested Data Sets this run could not reach and **emptied** for
|
|
4049
|
+
* it: an `SQ` whose items the parser did not materialize, so the run had no
|
|
4050
|
+
* Data Sets to walk and could not discharge PS3.15 §E.1.1's obligation inside
|
|
4051
|
+
* them, and a private carrier a {@link Profile} declares `SQ` that the parse
|
|
4052
|
+
* tree resolved otherwise. Content was dropped from the de-identified output,
|
|
4053
|
+
* and for the first producer the matching `DICOM_SQ_NOT_DESCENDED` entry on
|
|
4054
|
+
* `Dataset.warnings` says why the parse refused.
|
|
4055
|
+
*
|
|
4056
|
+
* 🛑 **THIS ARRAY NO LONGER PRODUCES `applied: "kept"`, AND THAT IS AN
|
|
4057
|
+
* AUDIT-CONTRACT CHANGE.** A private attribute retained under
|
|
4058
|
+
* `RetainSafePrivate` whose value this run never enumerated used to be listed
|
|
4059
|
+
* here, kept verbatim, with the array saying so. That instance is **removed**
|
|
4060
|
+
* now and recorded on
|
|
4061
|
+
* {@link DeidentifyReport.unenumerablePrivateRemovals}. An entry here has one
|
|
4062
|
+
* meaning again: this content is **not** in your output.
|
|
4063
|
+
*
|
|
4064
|
+
* **Empty on a well-formed file**, including one whose private attributes a
|
|
4065
|
+
* profile vouches for. It stopped being populated by ordinary conformant files
|
|
4066
|
+
* when the retained class left it.
|
|
2990
4067
|
*
|
|
2991
4068
|
* **Capped, and the cap is on the record only.** A crafted input can carry
|
|
2992
4069
|
* tens of thousands of un-auditable elements, so this array (and its matching
|
|
2993
4070
|
* warnings) stops at `MAX_UNAUDITABLE_SEQUENCE_FINDINGS`. Every un-auditable
|
|
2994
|
-
*
|
|
2995
|
-
* many", so read it as truncated rather than as a
|
|
4071
|
+
* carrier is still emptied whether or not it is listed; an array exactly that
|
|
4072
|
+
* long means "at least this many", so read it as truncated rather than as a
|
|
4073
|
+
* total. The unenumerable **removals** are budgeted apart from it and their
|
|
4074
|
+
* record is not capped at all.
|
|
2996
4075
|
*
|
|
2997
4076
|
* **It covers private sequences too, since `DICOM-PRIVATE-SQ-CARVE-OUT`.** A
|
|
2998
4077
|
* private `SQ` a {@link Profile} vouches for under `RetainSafePrivate` used to
|
|
@@ -3001,6 +4080,34 @@ interface DeidentifyReport {
|
|
|
3001
4080
|
* is over a private attribute and not over an item stream nothing could read.
|
|
3002
4081
|
*/
|
|
3003
4082
|
readonly unauditableSequences: readonly UnauditableSequenceFinding[];
|
|
4083
|
+
/**
|
|
4084
|
+
* Private attributes **removed because this run did not enumerate their
|
|
4085
|
+
* value** - one entry per instance, naming the tag, the outcome (`"removed"`),
|
|
4086
|
+
* the reason (`"unenumerable"`) and the Data Set it lived in.
|
|
4087
|
+
*
|
|
4088
|
+
* **This is the surface the fail-safe's audit half is built on, and it is the
|
|
4089
|
+
* one array on this report that is never capped or truncated.** A caller can
|
|
4090
|
+
* separate, from this record alone and for every such removal at any input
|
|
4091
|
+
* size, the attributes removed for being unenumerable from the attributes
|
|
4092
|
+
* removed by the Annex E action table (`report.attributes`) and from the ones
|
|
4093
|
+
* whose value was emptied. The matching warnings ARE bounded, so past their
|
|
4094
|
+
* cap this record is the only complete account and the diagnostics are the
|
|
4095
|
+
* bounded one.
|
|
4096
|
+
*
|
|
4097
|
+
* **Empty unless `RetainSafePrivate` plus a `Profile` is active**, because
|
|
4098
|
+
* without both nothing private is retained far enough to be judged: the Basic
|
|
4099
|
+
* Profile removes every private attribute and names it in
|
|
4100
|
+
* {@link DeidentifyReport.removedPrivateTags} instead. A profile lookup that
|
|
4101
|
+
* **misses** is that case too, and never appears here. **The file's own
|
|
4102
|
+
* (0008,0300) declaration does not populate it either**, and that is not an
|
|
4103
|
+
* omission: a value that declaration covers is one §E.3.10 says the run knows
|
|
4104
|
+
* about, so it is retained rather than reaching this rule at all.
|
|
4105
|
+
*
|
|
4106
|
+
* See {@link UnenumerablePrivateRemoval} for what counts as enumeration, for
|
|
4107
|
+
* the over-redaction this costs, and for why it names an instance rather than
|
|
4108
|
+
* a tag.
|
|
4109
|
+
*/
|
|
4110
|
+
readonly unenumerablePrivateRemovals: readonly UnenumerablePrivateRemoval[];
|
|
3004
4111
|
/**
|
|
3005
4112
|
* Elements emptied because their on-wire VR is not one of the 34 PS3.5 §6.2
|
|
3006
4113
|
* defines, so their bytes are not a Value Field this library decoded and
|
|
@@ -3040,6 +4147,82 @@ interface DeidentifyReport {
|
|
|
3040
4147
|
* because a vouched-for private `SQ` is routed into the ordinary `SQ` branches.
|
|
3041
4148
|
*/
|
|
3042
4149
|
readonly undefinedVrElements: readonly UndefinedVrFinding[];
|
|
4150
|
+
/**
|
|
4151
|
+
* Non-modeled `(0002,xxxx)` File Meta elements this run **dropped** instead of
|
|
4152
|
+
* re-emitting, because the de-identified File Meta group describes this
|
|
4153
|
+
* de-identifying application rather than the source (PS3.15 §E.1.1). Empty
|
|
4154
|
+
* when the source group held only modeled elements, and empty when the source
|
|
4155
|
+
* parsed with no File Meta group at all.
|
|
4156
|
+
*
|
|
4157
|
+
* **This is the record of a deliberate fidelity loss, not of a defect.** The
|
|
4158
|
+
* byte-for-byte File Meta round trip `FileMeta.extraElements` documents is
|
|
4159
|
+
* scoped to parse-then-serialize and does not hold here.
|
|
4160
|
+
*
|
|
4161
|
+
* **Capped; the count beside it is not.** See {@link FileMetaDroppedElement}
|
|
4162
|
+
* and {@link DeidentifyReport.fileMetaElementsDroppedCount}.
|
|
4163
|
+
*/
|
|
4164
|
+
readonly fileMetaElementsDropped: readonly FileMetaDroppedElement[];
|
|
4165
|
+
/**
|
|
4166
|
+
* How many non-modeled `(0002,xxxx)` elements this run dropped, **complete at
|
|
4167
|
+
* any input size**.
|
|
4168
|
+
*
|
|
4169
|
+
* The field exists because its sibling array is capped: a caller that needs to
|
|
4170
|
+
* *count* the fidelity loss cannot get the number from an array whose length
|
|
4171
|
+
* saturates, and re-parsing the output does not give it either - the elements
|
|
4172
|
+
* are gone from the output by design. One number cannot be inflated by an
|
|
4173
|
+
* attacker-chosen element count, so bounding it would buy nothing.
|
|
4174
|
+
*/
|
|
4175
|
+
readonly fileMetaElementsDroppedCount: number;
|
|
4176
|
+
/**
|
|
4177
|
+
* Data Elements whose Group Number is `0004`, **removed** at every depth this
|
|
4178
|
+
* run reached, as PS3.15 §E.1.1 requires "from any SOP Instance or DICOM File
|
|
4179
|
+
* other than a DICOMDIR File".
|
|
4180
|
+
*
|
|
4181
|
+
* Empty when the object carried none, and empty when it declared Media Storage
|
|
4182
|
+
* SOP Class UID `1.2.840.10008.1.3.10` - the carve-out, which is disclosed on
|
|
4183
|
+
* {@link DeidentifyReport.warnings} as
|
|
4184
|
+
* `DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED` rather than left to be
|
|
4185
|
+
* inferred from an empty array.
|
|
4186
|
+
*
|
|
4187
|
+
* **Capped; the count beside it is not.** See {@link Group0004Removal} and
|
|
4188
|
+
* {@link DeidentifyReport.group0004RemovalCount}.
|
|
4189
|
+
*/
|
|
4190
|
+
readonly group0004Removals: readonly Group0004Removal[];
|
|
4191
|
+
/**
|
|
4192
|
+
* How many `(0004,xxxx)` elements this run removed, **complete at any input
|
|
4193
|
+
* size**, for the reason
|
|
4194
|
+
* {@link DeidentifyReport.fileMetaElementsDroppedCount} states.
|
|
4195
|
+
*
|
|
4196
|
+
* It is also the field that tells the two §E.1.1 rules apart at a glance: this
|
|
4197
|
+
* one counts group-0004 removals, the other counts File Meta drops, and
|
|
4198
|
+
* neither can be read off the other.
|
|
4199
|
+
*/
|
|
4200
|
+
readonly group0004RemovalCount: number;
|
|
4201
|
+
/**
|
|
4202
|
+
* Non-private Data Elements **removed** at every depth this run reached
|
|
4203
|
+
* because this build's PS3.6 2026d registry does not carry their tag and Table
|
|
4204
|
+
* E.1-1 does not list it. See {@link UnregisteredElementRemoval} for the rule,
|
|
4205
|
+
* for what it costs on a newer-edition file, and for why an entry names a byte
|
|
4206
|
+
* offset and no tag.
|
|
4207
|
+
*
|
|
4208
|
+
* Empty on a run that removed none. A separate record from
|
|
4209
|
+
* {@link DeidentifyReport.attributes}, {@link DeidentifyReport.removedPrivateTags},
|
|
4210
|
+
* {@link DeidentifyReport.group0004Removals} and
|
|
4211
|
+
* {@link DeidentifyReport.undefinedVrElements}: an element removed here is on
|
|
4212
|
+
* none of those.
|
|
4213
|
+
*
|
|
4214
|
+
* **Capped; the count beside it is not.** See
|
|
4215
|
+
* {@link DeidentifyReport.unregisteredElementRemovalCount}.
|
|
4216
|
+
*/
|
|
4217
|
+
readonly unregisteredElementRemovals: readonly UnregisteredElementRemoval[];
|
|
4218
|
+
/**
|
|
4219
|
+
* How many elements this run removed under the rule
|
|
4220
|
+
* {@link DeidentifyReport.unregisteredElementRemovals} records, **complete at
|
|
4221
|
+
* any input size**, for the reason
|
|
4222
|
+
* {@link DeidentifyReport.fileMetaElementsDroppedCount} states. `0` on a run
|
|
4223
|
+
* that removed none.
|
|
4224
|
+
*/
|
|
4225
|
+
readonly unregisteredElementRemovalCount: number;
|
|
3043
4226
|
/**
|
|
3044
4227
|
* Source UID → replacement UID, for cross-file consistency. The **keys are
|
|
3045
4228
|
* document values**, not composed identifiers: this is the one field of the
|
|
@@ -3051,11 +4234,13 @@ interface DeidentifyReport {
|
|
|
3051
4234
|
/**
|
|
3052
4235
|
* The Retain/Clean options that were active for this run.
|
|
3053
4236
|
*
|
|
3054
|
-
* **Not a list of what survived.**
|
|
3055
|
-
* kept without appearing anywhere in this field - `(0012,0063)`
|
|
3056
|
-
* De-identification Method
|
|
3057
|
-
*
|
|
3058
|
-
*
|
|
4237
|
+
* **Not a list of what survived.** Registered attributes Table E.1-1 does not
|
|
4238
|
+
* list are kept without appearing anywhere in this field - `(0012,0063)`
|
|
4239
|
+
* De-identification Method and the prior Items of `(0012,0064)`
|
|
4240
|
+
* De-identification Method Code Sequence are the ones whose retention is
|
|
4241
|
+
* disclosed, as `DICOM_DEIDENT_METHOD_PRIOR_RETAINED` and
|
|
4242
|
+
* `DICOM_DEIDENT_METHOD_CODES_PRIOR_RETAINED` on
|
|
4243
|
+
* {@link DeidentifyReport.warnings}, because neither is an option set.
|
|
3059
4244
|
*/
|
|
3060
4245
|
readonly retained: readonly DeidentifyOption[];
|
|
3061
4246
|
}
|
|
@@ -3083,22 +4268,47 @@ interface DeidentifyOptions {
|
|
|
3083
4268
|
*/
|
|
3084
4269
|
readonly uidMap?: Map<string, string>;
|
|
3085
4270
|
/**
|
|
3086
|
-
* A
|
|
4271
|
+
* A {@link Profile} whose private-dictionary overlay names the
|
|
3087
4272
|
* known-safe private attributes to keep when `RetainSafePrivate` is active.
|
|
3088
|
-
*
|
|
4273
|
+
*
|
|
4274
|
+
* **Not the only way an attribute is known safe, and no longer required for
|
|
4275
|
+
* `RetainSafePrivate` to keep anything.** PS3.15 2026c §E.3.10 lists four ways
|
|
4276
|
+
* that knowledge may be established and a profile is two of them
|
|
4277
|
+
* (Conformance Statement documentation, and "some other means"); the first is
|
|
4278
|
+
* the **file's own** Private Data Element Characteristics Sequence
|
|
4279
|
+
* (0008,0300), which `deidentify()` reads when this Option is active whether
|
|
4280
|
+
* or not a profile is passed. So a file from a vendor you hold no profile for
|
|
4281
|
+
* keeps the private attributes **that file declares** non-identifying, and
|
|
4282
|
+
* passing a profile only ever adds to that: a profile retention survives a
|
|
4283
|
+
* block the declaration calls `UNSAFE` or says nothing about.
|
|
4284
|
+
*
|
|
4285
|
+
* 🩺 **What the declaration route retains rests on the SENDER's assertion.**
|
|
4286
|
+
* The system that wrote the file said those blocks carry no identifying
|
|
4287
|
+
* information; nothing here re-derives it, and an opaque value in a block
|
|
4288
|
+
* declared `SAFE` reaches de-identified output unexamined. If you do not trust
|
|
4289
|
+
* the sender, leave `RetainSafePrivate` off - §E.3.10's own alternative, and
|
|
4290
|
+
* the only mitigation the standard offers.
|
|
4291
|
+
*
|
|
4292
|
+
* With neither this nor a declaration in the file, `RetainSafePrivate` keeps
|
|
4293
|
+
* nothing (fail-safe).
|
|
3089
4294
|
*/
|
|
3090
4295
|
readonly profile?: Profile;
|
|
3091
4296
|
/**
|
|
3092
4297
|
* Text **added to** `(0012,0063)` De-identification Method. Default names the
|
|
3093
4298
|
* Basic Profile and the active options.
|
|
3094
4299
|
*
|
|
4300
|
+
* **It never changes `(0012,0064)`.** The CID 7050 codes the run writes there
|
|
4301
|
+
* follow the options that ran, and free text cannot alter what ran; see
|
|
4302
|
+
* `DEIDENTIFICATION_METHOD_CODES`.
|
|
4303
|
+
*
|
|
3095
4304
|
* **The default is multi-valued, one Value per name, and that is a conformance
|
|
3096
4305
|
* fix rather than a style choice.** PS3.5 2026c Table 6.2-1 caps an `LO` at
|
|
3097
4306
|
* **64 characters per Value**, and `(0012,0063)` is `1-n`; the single-value
|
|
3098
4307
|
* default this replaced measured 76 characters with no options and 272 with
|
|
3099
|
-
*
|
|
3100
|
-
* legally carry. The Profile name is now one
|
|
3101
|
-
* active option is its own, so no option
|
|
4308
|
+
* every option name published at that time, so every run this library ever
|
|
4309
|
+
* made wrote a value no `LO` may legally carry. The Profile name is now one
|
|
4310
|
+
* Value of 61 characters and each active option is its own, so no option
|
|
4311
|
+
* subset can exceed the maximum.
|
|
3102
4312
|
*
|
|
3103
4313
|
* **Your string is not bounded here, but it is no longer silent.** A value of
|
|
3104
4314
|
* your own longer than 64 characters is written through as given - it is
|
|
@@ -3175,14 +4385,27 @@ interface DeidentifyResult<TDataset> {
|
|
|
3175
4385
|
/**
|
|
3176
4386
|
* Stable codes for {@link DeidentifyError}.
|
|
3177
4387
|
*
|
|
4388
|
+
* - `INVALID_OPTIONS`: an author-time misconfiguration of the call itself (an
|
|
4389
|
+
* unknown Retain option, both PS3.15 §E.3.6 temporal Options at once, a
|
|
4390
|
+
* malformed `uidRoot`).
|
|
4391
|
+
* - `UNSUPPORTED_TRANSFER_SYNTAX`: the Dataset's File Meta Transfer Syntax UID
|
|
4392
|
+
* is one this de-identifier refuses to act on, whatever the options. Today
|
|
4393
|
+
* that is exactly the four JPIP Referenced syntaxes of PS3.5 2026c sections
|
|
4394
|
+
* A.6, A.7, A.11 and A.12: such an object references its pixels through
|
|
4395
|
+
* Pixel Data Provider URL `(0028,7FE0)`, which has no PS3.15 Table E.1-1 row,
|
|
4396
|
+
* so a de-identified copy would keep the URL by omission. Nothing is
|
|
4397
|
+
* de-identified and no dataset or report is returned.
|
|
4398
|
+
*
|
|
3178
4399
|
* @example
|
|
3179
4400
|
* ```ts
|
|
3180
4401
|
* import { DEIDENTIFY_ERROR_CODES } from "@cosyte/dicom";
|
|
3181
4402
|
* DEIDENTIFY_ERROR_CODES.INVALID_OPTIONS; // "INVALID_OPTIONS"
|
|
4403
|
+
* DEIDENTIFY_ERROR_CODES.UNSUPPORTED_TRANSFER_SYNTAX; // "UNSUPPORTED_TRANSFER_SYNTAX"
|
|
3182
4404
|
* ```
|
|
3183
4405
|
*/
|
|
3184
4406
|
declare const DEIDENTIFY_ERROR_CODES: Readonly<{
|
|
3185
4407
|
readonly INVALID_OPTIONS: "INVALID_OPTIONS";
|
|
4408
|
+
readonly UNSUPPORTED_TRANSFER_SYNTAX: "UNSUPPORTED_TRANSFER_SYNTAX";
|
|
3186
4409
|
}>;
|
|
3187
4410
|
/**
|
|
3188
4411
|
* One of the {@link DEIDENTIFY_ERROR_CODES} values.
|
|
@@ -3196,11 +4419,16 @@ declare const DEIDENTIFY_ERROR_CODES: Readonly<{
|
|
|
3196
4419
|
*/
|
|
3197
4420
|
type DeidentifyErrorCode = (typeof DEIDENTIFY_ERROR_CODES)[keyof typeof DEIDENTIFY_ERROR_CODES];
|
|
3198
4421
|
/**
|
|
3199
|
-
* Thrown
|
|
3200
|
-
*
|
|
3201
|
-
*
|
|
3202
|
-
*
|
|
3203
|
-
*
|
|
4422
|
+
* Thrown by {@link deidentify} when it will not de-identify, for one of two
|
|
4423
|
+
* reasons a caller tells apart by `code` (see {@link DEIDENTIFY_ERROR_CODES}):
|
|
4424
|
+
* an author-time misconfiguration of the call (`INVALID_OPTIONS`: an unknown
|
|
4425
|
+
* Retain option, a malformed UID root), or a Dataset under a Transfer Syntax this
|
|
4426
|
+
* de-identifier refuses (`UNSUPPORTED_TRANSFER_SYNTAX`: the four JPIP Referenced
|
|
4427
|
+
* syntaxes). Distinct from the parser's fatal codes, the value layer's
|
|
4428
|
+
* `DicomValueError`, and the serializer's `DicomSerializeError`. The message
|
|
4429
|
+
* carries only structural facts (option names, the UID root) or a fixed string -
|
|
4430
|
+
* never a decoded value, and for `UNSUPPORTED_TRANSFER_SYNTAX` nothing read from
|
|
4431
|
+
* the Dataset at all.
|
|
3204
4432
|
*
|
|
3205
4433
|
* @example
|
|
3206
4434
|
* ```ts
|
|
@@ -3245,17 +4473,41 @@ declare class DeidentifyError extends Error {
|
|
|
3245
4473
|
* exact-tag miss. A match is removed *and* reported, with
|
|
3246
4474
|
* `DeidentifiedAttribute.repeatingGroup` naming the mask.
|
|
3247
4475
|
* - Recurses into kept sequences and **re-encodes** them so nested PHI is removed
|
|
3248
|
-
* in the *serialized* bytes too - not just the object model (the
|
|
4476
|
+
* in the *serialized* bytes too - not just the object model (the writer
|
|
3249
4477
|
* blits `SQ` spans verbatim, so a rebuilt `items` array alone would not survive
|
|
3250
4478
|
* serialization). Rebuilt sequences are normalized to defined length.
|
|
3251
|
-
* - Removes
|
|
3252
|
-
*
|
|
3253
|
-
*
|
|
4479
|
+
* - Removes every non-private attribute Table E.1-1 does not list **and this
|
|
4480
|
+
* build's PS3.6 2026d registry does not carry** - a Standard Attribute from a
|
|
4481
|
+
* later edition, or one the sender invented - at every depth and whatever its
|
|
4482
|
+
* VR, recording it on `report.unregisteredElementRemovals` by byte offset and
|
|
4483
|
+
* never by tag. A registered attribute Table E.1-1 does not list is kept as it
|
|
4484
|
+
* always was. See {@link isUnregisteredStandardTag}.
|
|
4485
|
+
* - Removes all private attributes by default. With `RetainSafePrivate` it keeps
|
|
4486
|
+
* the ones PS3.15 §E.3.10 lets it know are safe, by two routes that only ever
|
|
4487
|
+
* add to each other: the private data elements a caller {@link Profile}'s
|
|
4488
|
+
* overlay names as safe (and the private-creator elements it recognizes), and
|
|
4489
|
+
* the blocks the **file itself** declares non-identifying in Private Data
|
|
4490
|
+
* Element Characteristics Sequence `(0008,0300)`, which needs no profile. The
|
|
4491
|
+
* second one is the sender's assertion rather than this run's finding; see
|
|
4492
|
+
* {@link declaredSafe}.
|
|
4493
|
+
* - Writes `(0028,0303)` Longitudinal Temporal Information Modified with the
|
|
4494
|
+
* state this run's option set put the object in: `UNMODIFIED` under
|
|
4495
|
+
* `RetainLongitudinalTemporal`, `MODIFIED` under
|
|
4496
|
+
* `RetainLongitudinalTemporalModifiedDates`, `REMOVED` under neither (PS3.15
|
|
4497
|
+
* §E.3.6 and §E.2). The two temporal Options are mutually exclusive and a call
|
|
4498
|
+
* naming both is rejected. **Replaced, not joined** - it is `VM 1`, so a prior
|
|
4499
|
+
* sender's declaration is discarded rather than appended to. `MODIFIED` says
|
|
4500
|
+
* the modified-dates column was resolved and **not** that this library
|
|
4501
|
+
* transformed a date, which it never does; see {@link TEMPORAL_STATE}.
|
|
3254
4502
|
* - Remaps `(0002,0003)` Media Storage SOP Instance UID consistently (unless
|
|
3255
4503
|
* `RetainUIDs`), writes `(0012,0062)` Patient Identity Removed = `YES`, **adds**
|
|
3256
4504
|
* its method text to `(0012,0063)` De-identification Method rather than
|
|
3257
4505
|
* replacing what the file already recorded there (PS3.15 E.1.1 - see
|
|
3258
|
-
* `addDeidentificationMethod`),
|
|
4506
|
+
* `addDeidentificationMethod`), **adds** the PS3.16 CID 7050 codes for the
|
|
4507
|
+
* Profile and each active Option to `(0012,0064)` De-identification Method Code
|
|
4508
|
+
* Sequence, after any Items the file already carried there and without
|
|
4509
|
+
* repeating one it already records (see `addDeidentificationMethodCodes`), and
|
|
4510
|
+
* warns
|
|
3259
4511
|
* (`DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED`) when Pixel Data is present and not
|
|
3260
4512
|
* marked free of burned-in annotation - this metadata-only pass cannot clean
|
|
3261
4513
|
* pixels (deferred to `@cosyte/dicom-pixel`).
|
|
@@ -3266,6 +4518,16 @@ declare class DeidentifyError extends Error {
|
|
|
3266
4518
|
* dummy is instead removed/emptied.
|
|
3267
4519
|
* - `C` (clean) is a conservative blank, not a meaning-preserving structured
|
|
3268
4520
|
* replacement (which needs domain context the metadata layer lacks).
|
|
4521
|
+
* - **A conformant attribute from a PS3.6 edition newer than this build's pin is
|
|
4522
|
+
* removed**, because it has no registry row here and nothing separates it from
|
|
4523
|
+
* an invented one. Over-redaction, deliberate, with no caller switch in this
|
|
4524
|
+
* release (`DICOM-DEIDENT-OVER-REDACTION`).
|
|
4525
|
+
* - The temporal declaration is written at the **top level only**, which is where
|
|
4526
|
+
* §E.2 and §E.3.6 put it and where a recipient reads it. `(0028,0303)` has no
|
|
4527
|
+
* row in Table E.1-1, so a copy the sender nested inside a Sequence Item is
|
|
4528
|
+
* retained by omission like any other unlisted attribute and still says
|
|
4529
|
+
* whatever that sender wrote. Read the Data Set's own `(0028,0303)`, never a
|
|
4530
|
+
* nested one.
|
|
3269
4531
|
* - Pixel-level options (`CleanPixelData`, `CleanRecognizableVisual`) are out of
|
|
3270
4532
|
* scope; burned-in text is warned, never cleaned.
|
|
3271
4533
|
* - A private data element kept under `RetainSafePrivate` is kept *verbatim*
|
|
@@ -3283,6 +4545,22 @@ declare class DeidentifyError extends Error {
|
|
|
3283
4545
|
* wins). A carrier the profile declares `SQ` and the tree did not resolve is
|
|
3284
4546
|
* emptied rather than kept, keeping its parsed VR
|
|
3285
4547
|
* (`DICOM-PRIVATE-SQ-PARSE-VR`). See {@link keepRetainedPrivate}.
|
|
4548
|
+
* - **A retained private value this run did not ENUMERATE is REMOVED**, named in
|
|
4549
|
+
* `report.removedPrivateTags` and recorded per instance in
|
|
4550
|
+
* `report.unenumerablePrivateRemovals` with its reason, under the existing
|
|
4551
|
+
* `DICOM_DEIDENT_PRIVATE_CARRIER_NOT_AUDITABLE` warning. Through `0.0.19` it
|
|
4552
|
+
* was kept verbatim and merely disclosed, which shipped a Data Set the sender
|
|
4553
|
+
* nested inside it into output stamped `(0012,0062) = YES` on a **fully
|
|
4554
|
+
* conformant** file. 🛑 **The cost is priced at its real size and is not a
|
|
4555
|
+
* corner of the option:** this package enumerates nothing inside a retained
|
|
4556
|
+
* private value beyond the classes named on
|
|
4557
|
+
* {@link removeUnenumerablePrivate}, so an ordinary vendor scalar under an
|
|
4558
|
+
* ordinary string VR that the file's own `(0008,0300)` declaration does not
|
|
4559
|
+
* name safe is removed too. Getting such a value back needs either a
|
|
4560
|
+
* declaration in the file - the sender's assertion, not an enumeration this
|
|
4561
|
+
* run performed - or the content test that separates a nested Data Set from a
|
|
4562
|
+
* legitimate binary blob, which is an open product question
|
|
4563
|
+
* (`DICOM-DEIDENT-OVER-REDACTION`).
|
|
3286
4564
|
* - A `SQ` whose `items` the parser did not
|
|
3287
4565
|
* materialize is **emptied**, not kept:
|
|
3288
4566
|
* its value is by PS3.5 §7.5.1 a stream of Data Sets, and a run that cannot
|
|
@@ -3334,7 +4612,7 @@ declare class DeidentifyError extends Error {
|
|
|
3334
4612
|
* which removal is the branch available here (`(0008,0307)` is not
|
|
3335
4613
|
* implemented). Every private element the recursion **reaches** in such an
|
|
3336
4614
|
* Item, at any depth below it, is removed and named in
|
|
3337
|
-
* `report.removedPrivateTags`. **The carve-out
|
|
4615
|
+
* `report.removedPrivateTags`. **The carve-out an earlier review refused for asserting
|
|
3338
4616
|
* away is CLOSED** (`DICOM-PRIVATE-SQ-CARVE-OUT`): `keepsPrivate` still decides
|
|
3339
4617
|
* before the descent, but a **private `SQ`** it vouches for now routes through
|
|
3340
4618
|
* {@link keepRetainedPrivate} into the ordinary `SQ` branches instead of being
|
|
@@ -3374,8 +4652,33 @@ declare class DeidentifyError extends Error {
|
|
|
3374
4652
|
* map. Everything in it is composed from static tables except `uidMap`, whose
|
|
3375
4653
|
* keys are the source UIDs the file carried: treat that field as PHI.
|
|
3376
4654
|
*
|
|
3377
|
-
*
|
|
3378
|
-
*
|
|
4655
|
+
* The returned Data Set says what this run did to its **dates** as well as to its
|
|
4656
|
+
* identity: `(0028,0303) Longitudinal Temporal Information Modified` carries
|
|
4657
|
+
* `UNMODIFIED` when `RetainLongitudinalTemporal` was active, `MODIFIED` when
|
|
4658
|
+
* `RetainLongitudinalTemporalModifiedDates` was, and `REMOVED` under neither,
|
|
4659
|
+
* replacing any value the source carried at that tag. It is **not** on
|
|
4660
|
+
* {@link DeidentifyReport} - like `(0012,0062)`, it is a statement the object
|
|
4661
|
+
* makes about itself, and the report's shape is unchanged by it. 🩺 A
|
|
4662
|
+
* `MODIFIED` means the modified-dates column was resolved; **you** perform the
|
|
4663
|
+
* date transformation §E.3.6 describes, and `report.warnings` carries
|
|
4664
|
+
* `DICOM_DEIDENT_DATES_NOT_TRANSFORMED` saying so.
|
|
4665
|
+
*
|
|
4666
|
+
* **A JPIP Referenced object is refused, not de-identified.** When
|
|
4667
|
+
* `ds.fileMeta.transferSyntaxUID` is one of the four JPIP Referenced syntaxes
|
|
4668
|
+
* (PS3.5 2026c sections A.6, A.7, A.11 and A.12), the call throws before it
|
|
4669
|
+
* reads the options or the Data Set, because that object's Pixel Data Provider
|
|
4670
|
+
* URL `(0028,7FE0)` has no PS3.15 Table E.1-1 action and would otherwise be kept
|
|
4671
|
+
* by omission. The refusal keys on the File Meta alone: a non-JPIP object that
|
|
4672
|
+
* carries `(0028,7FE0)` is de-identified like any other, and keeps it.
|
|
4673
|
+
*
|
|
4674
|
+
* @throws {@link DeidentifyError} (`UNSUPPORTED_TRANSFER_SYNTAX`) for a `Dataset`
|
|
4675
|
+
* whose File Meta Transfer Syntax UID is one of the four JPIP Referenced
|
|
4676
|
+
* syntaxes, whatever the options; its message is a fixed string. Checked
|
|
4677
|
+
* first, so it is the code such a call gets even when its options are
|
|
4678
|
+
* invalid too.
|
|
4679
|
+
* @throws {@link DeidentifyError} (`INVALID_OPTIONS`) for an unknown Retain option,
|
|
4680
|
+
* for both PS3.15 §E.3.6 temporal Options in one call, or for a malformed
|
|
4681
|
+
* `uidRoot`.
|
|
3379
4682
|
*
|
|
3380
4683
|
* @example
|
|
3381
4684
|
* ```ts
|
|
@@ -3451,4 +4754,90 @@ interface UidRemapper {
|
|
|
3451
4754
|
*/
|
|
3452
4755
|
declare function makeUidRemapper(root?: string, cache?: Map<string, string>): UidRemapper;
|
|
3453
4756
|
|
|
3454
|
-
|
|
4757
|
+
/**
|
|
4758
|
+
* PS3.16 2026d CID 7050 "De-identification Method", carried as pinned data, and
|
|
4759
|
+
* the map from what a `deidentify()` run activated to the code it records.
|
|
4760
|
+
*
|
|
4761
|
+
* PS3.15 2026c §E.1.1: "one or more codes from CID 7050 "De-identification
|
|
4762
|
+
* Method" corresponding to the Profile and Options used shall be added to
|
|
4763
|
+
* De-identification Method Code Sequence (0012,0064), and/or a text string
|
|
4764
|
+
* describing the method used shall be inserted in or added to De-identification
|
|
4765
|
+
* Method (0012,0063)." This package writes both halves: the text on
|
|
4766
|
+
* `(0012,0063)` and these codes on `(0012,0064)`.
|
|
4767
|
+
*
|
|
4768
|
+
* **A cited constant, not a vendored PS3.16 plus a generator.** The context group
|
|
4769
|
+
* is thirteen rows and its version, `20170914`, is unchanged between the PS3.15
|
|
4770
|
+
* 2026d edition this package vendors and the PS3.16 2026d page these rows are
|
|
4771
|
+
* quoted from (Type Extensible, UID `1.2.840.10008.6.1.925`, every row
|
|
4772
|
+
* designator `DCM`). The "Option" suffix is part of each Code Meaning.
|
|
4773
|
+
*
|
|
4774
|
+
* Two rows are carried and **never written**: `113101` Clean Pixel Data Option and
|
|
4775
|
+
* `113102` Clean Recognizable Visual Features Option. This is a metadata-only
|
|
4776
|
+
* de-identifier and cannot inspect pixels, which is why neither is a
|
|
4777
|
+
* {@link DeidentifyOption}.
|
|
4778
|
+
*
|
|
4779
|
+
* @module
|
|
4780
|
+
*/
|
|
4781
|
+
|
|
4782
|
+
/**
|
|
4783
|
+
* One row of PS3.16 CID 7050 "De-identification Method": the three Basic Coded
|
|
4784
|
+
* Entry Attributes (PS3.3 2026d Table 8.8-1a) of one code, exactly as the context
|
|
4785
|
+
* group spells them.
|
|
4786
|
+
*
|
|
4787
|
+
* `deidentify()` writes each code it records as one Item of
|
|
4788
|
+
* `(0012,0064)` De-identification Method Code Sequence carrying Code Value
|
|
4789
|
+
* `(0008,0100)`, Coding Scheme Designator `(0008,0102)` and Code Meaning
|
|
4790
|
+
* `(0008,0104)`, and nothing else. Coding Scheme Version is omitted because PS3.3
|
|
4791
|
+
* requires it only where the designator is not sufficient to identify the code,
|
|
4792
|
+
* and `DCM` is.
|
|
4793
|
+
*
|
|
4794
|
+
* @example
|
|
4795
|
+
* ```ts
|
|
4796
|
+
* import { DEIDENTIFICATION_METHOD_CODES, type DeidentificationMethodCode } from "@cosyte/dicom";
|
|
4797
|
+
* const profile: DeidentificationMethodCode = DEIDENTIFICATION_METHOD_CODES.profile;
|
|
4798
|
+
* profile.codeValue; // "113100"
|
|
4799
|
+
* ```
|
|
4800
|
+
*/
|
|
4801
|
+
interface DeidentificationMethodCode {
|
|
4802
|
+
/** Code Value `(0008,0100)`, a VR `SH` value. */
|
|
4803
|
+
readonly codeValue: string;
|
|
4804
|
+
/** Coding Scheme Designator `(0008,0102)`. Every CID 7050 row is `DCM`. */
|
|
4805
|
+
readonly codingSchemeDesignator: "DCM";
|
|
4806
|
+
/** Code Meaning `(0008,0104)`, a VR `LO` value, spelled as CID 7050 spells it. */
|
|
4807
|
+
readonly codeMeaning: string;
|
|
4808
|
+
}
|
|
4809
|
+
/**
|
|
4810
|
+
* PS3.16 2026d CID 7050 "De-identification Method" (context group version
|
|
4811
|
+
* `20170914`, UID `1.2.840.10008.6.1.925`), with the code `deidentify()` records
|
|
4812
|
+
* for the Profile and for each {@link DeidentifyOption}.
|
|
4813
|
+
*
|
|
4814
|
+
* - `rows` - all thirteen rows, in the context group's own order.
|
|
4815
|
+
* - `profile` - `113100` Basic Application Confidentiality Profile, written by
|
|
4816
|
+
* every run.
|
|
4817
|
+
* - `options` - the code written when that Option was active for the run. An
|
|
4818
|
+
* Option "ran" when it was active: the Annex E column it selects is resolved
|
|
4819
|
+
* off the option set, whether or not the object carried an attribute it acts
|
|
4820
|
+
* on. `113107` therefore says the modified-dates column was resolved, **not**
|
|
4821
|
+
* that any date was shifted; this library transforms no date, and every such
|
|
4822
|
+
* run carries `DICOM_DEIDENT_DATES_NOT_TRANSFORMED` on `report.warnings`.
|
|
4823
|
+
*
|
|
4824
|
+
* `113101` and `113102` are in `rows` and are never written: they name the two
|
|
4825
|
+
* pixel-level Options, which this metadata-only layer does not perform.
|
|
4826
|
+
*
|
|
4827
|
+
* @example
|
|
4828
|
+
* ```ts
|
|
4829
|
+
* import { DEIDENTIFICATION_METHOD_CODES } from "@cosyte/dicom";
|
|
4830
|
+
* DEIDENTIFICATION_METHOD_CODES.contextGroupUid; // "1.2.840.10008.6.1.925"
|
|
4831
|
+
* DEIDENTIFICATION_METHOD_CODES.options.RetainUIDs.codeValue; // "113110"
|
|
4832
|
+
* DEIDENTIFICATION_METHOD_CODES.rows.length; // 13
|
|
4833
|
+
* ```
|
|
4834
|
+
*/
|
|
4835
|
+
declare const DEIDENTIFICATION_METHOD_CODES: {
|
|
4836
|
+
readonly contextGroupUid: "1.2.840.10008.6.1.925";
|
|
4837
|
+
readonly contextGroupVersion: "20170914";
|
|
4838
|
+
readonly rows: readonly DeidentificationMethodCode[];
|
|
4839
|
+
readonly profile: DeidentificationMethodCode;
|
|
4840
|
+
readonly options: Readonly<Record<DeidentifyOption, DeidentificationMethodCode>>;
|
|
4841
|
+
};
|
|
4842
|
+
|
|
4843
|
+
export { type AppliedAction, CODING_SCHEME_OIDS, type CodedConcept, DEFAULT_UID_ROOT, DEIDENTIFICATION_METHOD_CODES, DEIDENTIFY_ERROR_CODES, DEIDENTIFY_OPTIONS, Dataset, type DateParts, type DefineProfileOptions, type DeidentificationMethodCode, type DeidentifiedAttribute, DeidentifyError, type DeidentifyErrorCode, type DeidentifyOption, type DeidentifyOptions, type DeidentifyReport, type DeidentifyResult, type DicomDate, type DicomDateTime, type DicomDirectory, DicomParseError, type DicomParseWarning, type DicomPosition, DicomSerializeError, type DicomTime, type DicomValue, DicomValueError, index as Dictionary, type DirectoryRecord, Element, type EmbeddedAttributeFinding, FATAL_CODES, type FatalCode, type FileMeta, type FileMetaDroppedElement, type FileMetaRawElement, type FrameFunctionalGroups, type Group0004Removal, type ImageView, Item, OFFSET_FRAMES, type OffsetFrame, type OnWarningCallback, type OtherPatientId, type ParseOptions, type PatientView, type PersonName, type PersonNameGroup, type PixelDataFragments, type PrivateTagDefinition, type Profile, ProfileDefinitionError, type ProfilePrivateTags, type RealWorldValueMap, SERIALIZE_ERROR_CODES, Sequence, type SerializeErrorCode, type SeriesView, type StudyView, type ToDateOptions, type UidRemapper, type UnauditableSequenceFinding, type UndefinedVrFinding, type UnenumerablePrivateRemoval, type UnregisteredElementRemoval, VALUE_ERROR_CODES, VERSION, type ValueErrorCode, WARNING_CODES, type WarningCode, codingSchemeOid, decodeElementValue, decodeText, defineProfile, deidentify, isKnownCharsetTerm, makeUidRemapper, parseDate, parseDateTime, parseDicom, parsePersonName, parseSpecificCharacterSet, parseTime, profiles, readCode, readPixelDataFragments, resolveDecoderLabel, serializeDicom, toDate, toISO, toObject };
|