@cosyte/dicom 0.0.6 → 0.0.8
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 +376 -0
- package/README.md +6 -2
- package/dist/index.cjs +276 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +276 -10
- package/dist/index.d.ts +276 -10
- package/dist/index.mjs +276 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
package/dist/index.d.cts
CHANGED
|
@@ -415,6 +415,9 @@ declare const WARNING_CODES: {
|
|
|
415
415
|
readonly DICOM_CHARSET_AMBIGUOUS_SEPARATOR: "DICOM_CHARSET_AMBIGUOUS_SEPARATOR";
|
|
416
416
|
readonly DICOM_UNSUPPORTED_CHARSET: "DICOM_UNSUPPORTED_CHARSET";
|
|
417
417
|
readonly DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED: "DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED";
|
|
418
|
+
readonly DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED: "DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED";
|
|
419
|
+
readonly DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE: "DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE";
|
|
420
|
+
readonly DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE: "DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE";
|
|
418
421
|
readonly DICOM_PRIVATE_CREATOR_UNKNOWN: "DICOM_PRIVATE_CREATOR_UNKNOWN";
|
|
419
422
|
};
|
|
420
423
|
/**
|
|
@@ -2348,6 +2351,178 @@ interface DeidentifiedAttribute {
|
|
|
2348
2351
|
*/
|
|
2349
2352
|
readonly repeatingGroup?: string;
|
|
2350
2353
|
}
|
|
2354
|
+
/**
|
|
2355
|
+
* One value that was emptied because a Data Element was found **inside** it.
|
|
2356
|
+
*
|
|
2357
|
+
* PS3.5 defines Value Length as the length of that element's own Value Field. A
|
|
2358
|
+
* sender that over-declares it produces a file whose reading is self-consistent
|
|
2359
|
+
* and whose next element has been absorbed into the previous one's value - and
|
|
2360
|
+
* Table E.1-1 is keyed by tag, so an absorbed `(0010,0020)` is not an attribute
|
|
2361
|
+
* any longer and no action fires on it. `deidentify` therefore refuses to keep a
|
|
2362
|
+
* value whose tail decodes as whole Data Elements it would have acted on
|
|
2363
|
+
* (PS3.15 §E.1 "all instances"; §E.3.5 is the standard's own precedent for
|
|
2364
|
+
* removing identifying information embedded inside a string attribute).
|
|
2365
|
+
*
|
|
2366
|
+
* Every field is structural: `tag` and `vr` are the carrier's, and `hidden`
|
|
2367
|
+
* holds tags composed from four bytes each. No decoded value appears here, so
|
|
2368
|
+
* this is safe to log.
|
|
2369
|
+
*
|
|
2370
|
+
* @example
|
|
2371
|
+
* ```ts
|
|
2372
|
+
* import { deidentify, parseDicom } from "@cosyte/dicom";
|
|
2373
|
+
* const { report } = deidentify(parseDicom(buf));
|
|
2374
|
+
* for (const e of report.embeddedAttributes) {
|
|
2375
|
+
* console.warn(`${e.tag} hid ${e.hidden.join(", ")} in its value`);
|
|
2376
|
+
* }
|
|
2377
|
+
* ```
|
|
2378
|
+
*/
|
|
2379
|
+
interface EmbeddedAttributeFinding {
|
|
2380
|
+
/** The carrier - the element whose over-declared value held the others. */
|
|
2381
|
+
readonly tag: Tag;
|
|
2382
|
+
/** The carrier's VR. Always one of the string VRs; binary VRs are not scanned. */
|
|
2383
|
+
readonly vr: VR;
|
|
2384
|
+
/** The tags of the Data Elements found inside the carrier's value, in wire order. */
|
|
2385
|
+
readonly hidden: readonly Tag[];
|
|
2386
|
+
/** Tag/index chain when the carrier is inside a sequence item; omitted at the root. */
|
|
2387
|
+
readonly contextPath?: readonly string[];
|
|
2388
|
+
}
|
|
2389
|
+
/**
|
|
2390
|
+
* One `SQ` element that was emptied because the parser never materialized its
|
|
2391
|
+
* items, so the de-identifier had no Data Sets to walk.
|
|
2392
|
+
*
|
|
2393
|
+
* PS3.5 2026c §7.5.1 "Item Encoding Rules" states that "Each Item Value shall
|
|
2394
|
+
* contain a DICOM Data Set composed of Data Elements", so an `SQ` element's
|
|
2395
|
+
* value is never opaque bytes - it is Data Elements this run is obliged to
|
|
2396
|
+
* reach. PS3.15 2026c §E.1.1 "De-identifier" states that obligation directly:
|
|
2397
|
+
* an implementation claiming the Basic Application Level Confidentiality
|
|
2398
|
+
* Profile "shall protect or retain all instances of the Attributes listed in
|
|
2399
|
+
* [Table E.1-1], whether contained in the top level Data Set or embedded in an
|
|
2400
|
+
* Item of a Sequence of Items". When the item stream cannot be enumerated the
|
|
2401
|
+
* obligation cannot be discharged element by element, so it falls on the
|
|
2402
|
+
* enclosing attribute - the escalation §E.1.1 itself uses for a SOP Instance
|
|
2403
|
+
* UID inside a Sequence, where "the enclosing Attribute in the top-level Data
|
|
2404
|
+
* Set must be encrypted in its entirety". (That sentence is written about the
|
|
2405
|
+
* encrypt-and-replace mechanism for SOP Instance UIDs, not about Table E.1-1
|
|
2406
|
+
* generally; it is cited here as the standard's own precedent for escalating to
|
|
2407
|
+
* the carrier, not as a rule about this case.)
|
|
2408
|
+
*
|
|
2409
|
+
* Both fields are structural: `tag` is the carrier's and `byteLength` is the
|
|
2410
|
+
* declared size of the value that was dropped. No decoded value appears here,
|
|
2411
|
+
* so this is safe to log.
|
|
2412
|
+
*
|
|
2413
|
+
* The parser always announces the underlying refusal first, on
|
|
2414
|
+
* `Dataset.warnings`: `DICOM_SQ_NOT_DESCENDED` for a defined-length Implicit VR
|
|
2415
|
+
* LE value whose dictionary-resolved `SQ` was not a valid item stream.
|
|
2416
|
+
*
|
|
2417
|
+
* @example
|
|
2418
|
+
* ```ts
|
|
2419
|
+
* import { deidentify, parseDicom } from "@cosyte/dicom";
|
|
2420
|
+
* const { report } = deidentify(parseDicom(buf));
|
|
2421
|
+
* for (const s of report.unauditableSequences) {
|
|
2422
|
+
* console.warn(`${s.tag}: ${String(s.byteLength)} bytes dropped, item stream unreadable`);
|
|
2423
|
+
* }
|
|
2424
|
+
* ```
|
|
2425
|
+
*/
|
|
2426
|
+
interface UnauditableSequenceFinding {
|
|
2427
|
+
/** The `SQ` element that was emptied. */
|
|
2428
|
+
readonly tag: Tag;
|
|
2429
|
+
/** Byte length of the value field that was dropped. Structural, never a value. */
|
|
2430
|
+
readonly byteLength: number;
|
|
2431
|
+
/** Tag/index chain when the carrier is inside a sequence item; omitted at the root. */
|
|
2432
|
+
readonly contextPath?: readonly string[];
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* One element that was emptied because its **on-wire VR is not one of the 34
|
|
2436
|
+
* PS3.5 section 6.2 defines**, so nothing this library did to its bytes counts
|
|
2437
|
+
* as decoding a Value Field.
|
|
2438
|
+
*
|
|
2439
|
+
* ## Why such an element exists at all
|
|
2440
|
+
*
|
|
2441
|
+
* Under an Explicit VR Transfer Syntax the VR is two bytes the sender wrote, and
|
|
2442
|
+
* this parser trusts them (Postel's Law on the read path). The routine way two
|
|
2443
|
+
* arbitrary bytes end up in a VR field is an **under**-declared Value Length
|
|
2444
|
+
* upstream: the reader finishes the short value, and the leftover bytes of the
|
|
2445
|
+
* value that was actually encoded are read as the next Data Element header. Tag,
|
|
2446
|
+
* VR and length are then all fragments of somebody's value, and the element that
|
|
2447
|
+
* genuinely followed is consumed as this fabricated element's "value".
|
|
2448
|
+
*
|
|
2449
|
+
* Measured on `scripts/measure-sq-bound-grid.ts`: a carrier under-declaring by 6
|
|
2450
|
+
* produces `(4156,554C)` with the VR bytes `"E "`, whose value holds the source
|
|
2451
|
+
* `(0010,0020)` Patient ID in full. It reaches **string** carriers exactly as it
|
|
2452
|
+
* reaches binary ones, because the carrier's own VR is not what decides it.
|
|
2453
|
+
*
|
|
2454
|
+
* ## Why emptying, and why it is not a guess
|
|
2455
|
+
*
|
|
2456
|
+
* PS3.5 2026c section 6.2 requires every VR not yet defined to use the long-form
|
|
2457
|
+
* Data Element Structure - "with reserved bytes after the VR and a 32-bit
|
|
2458
|
+
* unsigned integer VL" - so an unrecognized VR read short-form, which is what
|
|
2459
|
+
* this parser does, is by the standard's own structure rule not a reading of a
|
|
2460
|
+
* Value Field. There is nothing to prove about the content: the test is a
|
|
2461
|
+
* membership check against the closed 34-VR set on a field the parser already
|
|
2462
|
+
* recorded, so there is no scan, no per-offset loop, and no cost that follows an
|
|
2463
|
+
* attacker-chosen value length.
|
|
2464
|
+
*
|
|
2465
|
+
* PS3.15 2026c section E.1.1 obliges an implementation claiming the Basic
|
|
2466
|
+
* Application Level Confidentiality Profile to "protect or retain all instances
|
|
2467
|
+
* of the Attributes listed in [Table E.1-1]". Those instances cannot be reached
|
|
2468
|
+
* inside bytes that were never a value, so the obligation falls on the carrier.
|
|
2469
|
+
*
|
|
2470
|
+
* **`UN` is not this.** `UN` is one of the 34, so an ordinary unknown-VR element
|
|
2471
|
+
* - the Implicit VR fallback for a tag this build's dictionary does not publish,
|
|
2472
|
+
* and the CP-246 shape - never reaches here. That is the line the sibling
|
|
2473
|
+
* `SQ`-with-no-items rule could not draw.
|
|
2474
|
+
*
|
|
2475
|
+
* ## Why this finding names no tag, when every sibling finding does
|
|
2476
|
+
*
|
|
2477
|
+
* Because the tag **may be content**, and nothing here can tell. The paragraph
|
|
2478
|
+
* above is the whole argument: when an under-declare desynchronized the reader,
|
|
2479
|
+
* the four tag bytes and the two VR bytes were read out of the middle of some
|
|
2480
|
+
* element's Value Field, so reporting the "tag" would republish four bytes of
|
|
2481
|
+
* the document. An unrecognized VR written honestly, at a correct length, raises
|
|
2482
|
+
* this same code and has an ordinary tag - **and the two are indistinguishable
|
|
2483
|
+
* here**, so the tag is withheld on both routes rather than on a guess. Measured on a synthetic `ST` carrier holding
|
|
2484
|
+
* `"MR BRAIN SMITHSON"`, the fabricated tag is `48544F53` - four bytes of the
|
|
2485
|
+
* surname. {@link EmbeddedAttributeFinding} and
|
|
2486
|
+
* {@link UnauditableSequenceFinding} may carry a tag because theirs came from a
|
|
2487
|
+
* header the sender really wrote; this one may not, and the asymmetry is the
|
|
2488
|
+
* finding rather than an inconsistency.
|
|
2489
|
+
*
|
|
2490
|
+
* `byteOffset` locates the element instead - a position this parser counted.
|
|
2491
|
+
* Nothing here renders a document byte: an offset the parser counted, a decoded
|
|
2492
|
+
* length, and the structural `contextPath`. Safe to log.
|
|
2493
|
+
*
|
|
2494
|
+
* @example
|
|
2495
|
+
* ```ts
|
|
2496
|
+
* import { deidentify, parseDicom } from "@cosyte/dicom";
|
|
2497
|
+
* const { report } = deidentify(parseDicom(buf));
|
|
2498
|
+
* for (const u of report.undefinedVrElements) {
|
|
2499
|
+
* console.warn(`offset ${String(u.byteOffset)}: ${String(u.byteLength)} bytes dropped`);
|
|
2500
|
+
* }
|
|
2501
|
+
* ```
|
|
2502
|
+
*/
|
|
2503
|
+
interface UndefinedVrFinding {
|
|
2504
|
+
/**
|
|
2505
|
+
* Byte offset of the emptied element's header. **This is how the element is
|
|
2506
|
+
* identified, and there is deliberately no `tag` field** - see the note above:
|
|
2507
|
+
* a fabricated header's tag bytes are part of some element's value.
|
|
2508
|
+
*/
|
|
2509
|
+
readonly byteOffset: number;
|
|
2510
|
+
/**
|
|
2511
|
+
* Byte length of the value field that was dropped.
|
|
2512
|
+
*
|
|
2513
|
+
* **An input-derived count, and on this finding specifically that is not a
|
|
2514
|
+
* formality.** For the sibling findings the length came from a header the
|
|
2515
|
+
* sender wrote; here the two length bytes can themselves be value bytes, like
|
|
2516
|
+
* the tag bytes. What is published is the *number* they decode to, never the
|
|
2517
|
+
* bytes - the same footing as every `{n}` in the warning registry, which is
|
|
2518
|
+
* documented there as "an input-derived count". A number is not a rendering,
|
|
2519
|
+
* and the reach is at most a character or so, but do not describe this field
|
|
2520
|
+
* as "structural, never a value" the way its siblings are described.
|
|
2521
|
+
*/
|
|
2522
|
+
readonly byteLength: number;
|
|
2523
|
+
/** Tag/index chain when the carrier is inside a sequence item; omitted at the root. */
|
|
2524
|
+
readonly contextPath?: readonly string[];
|
|
2525
|
+
}
|
|
2351
2526
|
/**
|
|
2352
2527
|
* The audit trail returned alongside the de-identified dataset.
|
|
2353
2528
|
*
|
|
@@ -2372,6 +2547,69 @@ interface DeidentifyReport {
|
|
|
2372
2547
|
readonly attributes: readonly DeidentifiedAttribute[];
|
|
2373
2548
|
/** Private tags removed under the Basic Profile (kept ones are omitted). */
|
|
2374
2549
|
readonly removedPrivateTags: readonly Tag[];
|
|
2550
|
+
/**
|
|
2551
|
+
* Values emptied because whole Data Elements were encoded inside them by an
|
|
2552
|
+
* over-declared Value Length. Empty on a well-formed file; a non-empty array
|
|
2553
|
+
* means the *source* was malformed in a way that hid attributes from the
|
|
2554
|
+
* action table, so treat it as a data-quality alarm on the sender as well as
|
|
2555
|
+
* an audit line. See {@link EmbeddedAttributeFinding}.
|
|
2556
|
+
*/
|
|
2557
|
+
readonly embeddedAttributes: readonly EmbeddedAttributeFinding[];
|
|
2558
|
+
/**
|
|
2559
|
+
* `SQ` elements emptied because the parser did not materialize their items, so
|
|
2560
|
+
* the run had no Data Sets to walk and could not discharge PS3.15 §E.1.1's
|
|
2561
|
+
* obligation inside them. Empty on a well-formed file; a non-empty array means
|
|
2562
|
+
* content was dropped from the de-identified output, and the matching
|
|
2563
|
+
* `DICOM_SQ_NOT_DESCENDED` entry on `Dataset.warnings` says why the parse
|
|
2564
|
+
* refused. See {@link UnauditableSequenceFinding}.
|
|
2565
|
+
*
|
|
2566
|
+
* **Capped, and the cap is on the record only.** A crafted input can carry
|
|
2567
|
+
* tens of thousands of un-auditable elements, so this array (and its matching
|
|
2568
|
+
* warnings) stops at `MAX_UNAUDITABLE_SEQUENCE_FINDINGS`. Every un-auditable
|
|
2569
|
+
* sequence is still emptied; an array exactly that long means "at least this
|
|
2570
|
+
* many", so read it as truncated rather than as a total.
|
|
2571
|
+
*
|
|
2572
|
+
* **It is not a complete list of what went un-audited, either**: a private
|
|
2573
|
+
* `SQ` a {@link Profile} vouches for under `RetainSafePrivate` is kept
|
|
2574
|
+
* verbatim and never appears here.
|
|
2575
|
+
*/
|
|
2576
|
+
readonly unauditableSequences: readonly UnauditableSequenceFinding[];
|
|
2577
|
+
/**
|
|
2578
|
+
* Elements emptied because their on-wire VR is not one of the 34 PS3.5 §6.2
|
|
2579
|
+
* defines, so their bytes are not a Value Field this library decoded and
|
|
2580
|
+
* PS3.15 §E.1.1's obligation over what is inside them could not be discharged.
|
|
2581
|
+
* Empty on a file conformant to **PS3.5 2026c**: a sender that writes one of
|
|
2582
|
+
* the 34 VRs that edition defines never produces one, and an Implicit VR LE
|
|
2583
|
+
* file **cannot** - there the VR comes from the dictionary. The edition is not
|
|
2584
|
+
* pedantry: §6.2 exists precisely to say how a *future* VR will be encoded, so
|
|
2585
|
+
* a file conformant to a later edition using a newly defined VR is the
|
|
2586
|
+
* population that sentence exists for. (What such a file does on this
|
|
2587
|
+
* library's parse path is not summarized here - it was measured and the
|
|
2588
|
+
* shapes disagree.) A non-empty array
|
|
2589
|
+
* means the source desynchronized the reader, usually by under-declaring a
|
|
2590
|
+
* Value Length somewhere earlier. See {@link UndefinedVrFinding}.
|
|
2591
|
+
*
|
|
2592
|
+
* **Capped, and the cap is on the record only**, exactly as
|
|
2593
|
+
* {@link DeidentifyReport.unauditableSequences} is: a crafted 1 MiB input can
|
|
2594
|
+
* carry over a hundred thousand such elements, so this array and its matching
|
|
2595
|
+
* warnings stop at `MAX_UNDEFINED_VR_FINDINGS`. Every one of them is still
|
|
2596
|
+
* emptied; an array exactly that long means "at least this many".
|
|
2597
|
+
*
|
|
2598
|
+
* **A finding here names a byte offset, not a tag** - uniquely among the
|
|
2599
|
+
* report's findings, and for a reason worth reading in
|
|
2600
|
+
* {@link UndefinedVrFinding}: the tag of a fabricated header is itself part of
|
|
2601
|
+
* some element's value.
|
|
2602
|
+
*
|
|
2603
|
+
* Unlike its sibling this list has **no carve-out**, and the reason is
|
|
2604
|
+
* structural rather than a promise: `keepOrEmpty` is the **only** path that
|
|
2605
|
+
* writes a source value into de-identified output unchanged, and the test sits
|
|
2606
|
+
* at the top of it. Every other outcome - `X` remove, `Z`/`C` empty, `D` dummy,
|
|
2607
|
+
* `U` remap, and a private tag the Basic Profile drops - already replaces the
|
|
2608
|
+
* value. So a `RetainSafePrivate` element a {@link Profile} vouches for still
|
|
2609
|
+
* reaches this test and is still emptied, which is where the sibling
|
|
2610
|
+
* `SQ`-with-no-items rule has a real carve-out and this one does not.
|
|
2611
|
+
*/
|
|
2612
|
+
readonly undefinedVrElements: readonly UndefinedVrFinding[];
|
|
2375
2613
|
/**
|
|
2376
2614
|
* Source UID → replacement UID, for cross-file consistency. The **keys are
|
|
2377
2615
|
* document values**, not composed identifiers: this is the one field of the
|
|
@@ -2529,15 +2767,43 @@ declare class DeidentifyError extends Error {
|
|
|
2529
2767
|
* - A private data element kept under `RetainSafePrivate` is kept *verbatim* - if
|
|
2530
2768
|
* it is itself a sequence carrying standard PHI attributes, that nested content
|
|
2531
2769
|
* is not recursed. The profile vouches the element is safe; nest accordingly.
|
|
2532
|
-
* - A
|
|
2533
|
-
*
|
|
2534
|
-
*
|
|
2535
|
-
*
|
|
2536
|
-
*
|
|
2537
|
-
*
|
|
2538
|
-
*
|
|
2539
|
-
*
|
|
2540
|
-
* `
|
|
2770
|
+
* - A **standard** (non-private) `SQ` whose `items` the parser did not
|
|
2771
|
+
* materialize is **emptied**, not kept:
|
|
2772
|
+
* its value is by PS3.5 §7.5.1 a stream of Data Sets, and a run that cannot
|
|
2773
|
+
* enumerate them cannot discharge §E.1.1's obligation inside them, so the
|
|
2774
|
+
* fail-safe answer is to drop the carrier and say so
|
|
2775
|
+
* (`DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE` + `report.unauditableSequences`).
|
|
2776
|
+
* That costs content, and the trade is deliberate: it used to be *kept
|
|
2777
|
+
* verbatim*, which wrote the sender's own un-audited Data Elements - measured,
|
|
2778
|
+
* `(0010,0020)` Patient IDs among them - into output stamped
|
|
2779
|
+
* `(0012,0062) PatientIdentityRemoved = YES`
|
|
2780
|
+
* (`DICOM-DEIDENT-RAWBYTES-PASSTHROUGH`, live through `0.0.6`).
|
|
2781
|
+
* - Two things are still kept verbatim, deliberately. A **private** `SQ`
|
|
2782
|
+
* retained under `RetainSafePrivate` + a {@link Profile}, where the profile has
|
|
2783
|
+
* vouched for the element by creator and tag - the pre-existing "kept
|
|
2784
|
+
* verbatim" limitation above, unchanged. And an undefined-length **`UN`** whose
|
|
2785
|
+
* CP-246 descent was refused: it keeps `vr === "UN"`, and since every ordinary
|
|
2786
|
+
* `UN` element also has no items, the test above cannot be applied there
|
|
2787
|
+
* without emptying every unknown-VR element in every file. That one is
|
|
2788
|
+
* measured and **still leaks** (`PRE-EXISTING`); the reliable consumer-side
|
|
2789
|
+
* test remains `el.items === undefined` on a `UN` element you are trusting a
|
|
2790
|
+
* report about.
|
|
2791
|
+
* - An element whose **on-wire VR is not one of the 34** PS3.5 §6.2 defines is
|
|
2792
|
+
* **emptied**, not kept (`DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE` +
|
|
2793
|
+
* `report.undefinedVrElements`). Such an element is what an *under*-declared
|
|
2794
|
+
* Value Length upstream produces: the reader desynchronizes, reads leftover
|
|
2795
|
+
* value bytes as a Data Element header, and the element that genuinely
|
|
2796
|
+
* followed becomes its "value" - measured carrying a `(0010,0020)` Patient ID
|
|
2797
|
+
* in full. §6.2 requires every undefined VR to be long-form, and this parser
|
|
2798
|
+
* reads it short-form, so those bytes are not a Value Field this library
|
|
2799
|
+
* decoded under any VR. See {@link hasUndefinedVr}. **Unlike the `SQ` rule
|
|
2800
|
+
* above this one has no carve-out** - it sits in `keepOrEmpty`, the only path
|
|
2801
|
+
* that keeps a value verbatim, so `RetainSafePrivate` does not exempt it.
|
|
2802
|
+
* - **Still leaking, measured, and its own decision:** the over-declare swallow
|
|
2803
|
+
* into a **binary** carrier (`OB`/`OW`/`US`/`UN`), 11 grid cells at
|
|
2804
|
+
* `35adc2d`. No content test can decide it - arbitrary bytes are what those
|
|
2805
|
+
* VRs are for - and the one candidate remedy empties conformant binary values.
|
|
2806
|
+
* See `./embedded.ts`.
|
|
2541
2807
|
*
|
|
2542
2808
|
* @module
|
|
2543
2809
|
*/
|
|
@@ -2628,4 +2894,4 @@ interface UidRemapper {
|
|
|
2628
2894
|
*/
|
|
2629
2895
|
declare function makeUidRemapper(root?: string, cache?: Map<string, string>): UidRemapper;
|
|
2630
2896
|
|
|
2631
|
-
export { type AppliedAction, CODING_SCHEME_OIDS, type CodedConcept, DEFAULT_UID_ROOT, DEIDENTIFY_ERROR_CODES, DEIDENTIFY_OPTIONS, Dataset, type DefineProfileOptions, type DeidentifiedAttribute, DeidentifyError, type DeidentifyErrorCode, type DeidentifyOption, type DeidentifyOptions, type DeidentifyReport, type DeidentifyResult, type DicomDate, type DicomDateTime, DicomParseError, type DicomParseWarning, type DicomPosition, DicomSerializeError, type DicomTime, type DicomValue, DicomValueError, index as Dictionary, Element, FATAL_CODES, type FatalCode, type FileMeta, type FileMetaRawElement, type FrameFunctionalGroups, type ImageView, Item, type OnWarningCallback, type OtherPatientId, type ParseOptions, type PatientView, type PersonName, type PersonNameGroup, type PrivateTagDefinition, type Profile, ProfileDefinitionError, type ProfilePrivateTags, type RealWorldValueMap, SERIALIZE_ERROR_CODES, Sequence, type SerializeErrorCode, type SeriesView, type StudyView, type UidRemapper, 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, resolveDecoderLabel, serializeDicom };
|
|
2897
|
+
export { type AppliedAction, CODING_SCHEME_OIDS, type CodedConcept, DEFAULT_UID_ROOT, DEIDENTIFY_ERROR_CODES, DEIDENTIFY_OPTIONS, Dataset, type DefineProfileOptions, type DeidentifiedAttribute, DeidentifyError, type DeidentifyErrorCode, type DeidentifyOption, type DeidentifyOptions, type DeidentifyReport, type DeidentifyResult, type DicomDate, type DicomDateTime, DicomParseError, type DicomParseWarning, type DicomPosition, DicomSerializeError, type DicomTime, type DicomValue, DicomValueError, index as Dictionary, Element, type EmbeddedAttributeFinding, FATAL_CODES, type FatalCode, type FileMeta, type FileMetaRawElement, type FrameFunctionalGroups, type ImageView, Item, type OnWarningCallback, type OtherPatientId, type ParseOptions, type PatientView, type PersonName, type PersonNameGroup, type PrivateTagDefinition, type Profile, ProfileDefinitionError, type ProfilePrivateTags, type RealWorldValueMap, SERIALIZE_ERROR_CODES, Sequence, type SerializeErrorCode, type SeriesView, type StudyView, type UidRemapper, type UnauditableSequenceFinding, type UndefinedVrFinding, 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, resolveDecoderLabel, serializeDicom };
|
package/dist/index.d.ts
CHANGED
|
@@ -415,6 +415,9 @@ declare const WARNING_CODES: {
|
|
|
415
415
|
readonly DICOM_CHARSET_AMBIGUOUS_SEPARATOR: "DICOM_CHARSET_AMBIGUOUS_SEPARATOR";
|
|
416
416
|
readonly DICOM_UNSUPPORTED_CHARSET: "DICOM_UNSUPPORTED_CHARSET";
|
|
417
417
|
readonly DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED: "DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED";
|
|
418
|
+
readonly DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED: "DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED";
|
|
419
|
+
readonly DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE: "DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE";
|
|
420
|
+
readonly DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE: "DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE";
|
|
418
421
|
readonly DICOM_PRIVATE_CREATOR_UNKNOWN: "DICOM_PRIVATE_CREATOR_UNKNOWN";
|
|
419
422
|
};
|
|
420
423
|
/**
|
|
@@ -2348,6 +2351,178 @@ interface DeidentifiedAttribute {
|
|
|
2348
2351
|
*/
|
|
2349
2352
|
readonly repeatingGroup?: string;
|
|
2350
2353
|
}
|
|
2354
|
+
/**
|
|
2355
|
+
* One value that was emptied because a Data Element was found **inside** it.
|
|
2356
|
+
*
|
|
2357
|
+
* PS3.5 defines Value Length as the length of that element's own Value Field. A
|
|
2358
|
+
* sender that over-declares it produces a file whose reading is self-consistent
|
|
2359
|
+
* and whose next element has been absorbed into the previous one's value - and
|
|
2360
|
+
* Table E.1-1 is keyed by tag, so an absorbed `(0010,0020)` is not an attribute
|
|
2361
|
+
* any longer and no action fires on it. `deidentify` therefore refuses to keep a
|
|
2362
|
+
* value whose tail decodes as whole Data Elements it would have acted on
|
|
2363
|
+
* (PS3.15 §E.1 "all instances"; §E.3.5 is the standard's own precedent for
|
|
2364
|
+
* removing identifying information embedded inside a string attribute).
|
|
2365
|
+
*
|
|
2366
|
+
* Every field is structural: `tag` and `vr` are the carrier's, and `hidden`
|
|
2367
|
+
* holds tags composed from four bytes each. No decoded value appears here, so
|
|
2368
|
+
* this is safe to log.
|
|
2369
|
+
*
|
|
2370
|
+
* @example
|
|
2371
|
+
* ```ts
|
|
2372
|
+
* import { deidentify, parseDicom } from "@cosyte/dicom";
|
|
2373
|
+
* const { report } = deidentify(parseDicom(buf));
|
|
2374
|
+
* for (const e of report.embeddedAttributes) {
|
|
2375
|
+
* console.warn(`${e.tag} hid ${e.hidden.join(", ")} in its value`);
|
|
2376
|
+
* }
|
|
2377
|
+
* ```
|
|
2378
|
+
*/
|
|
2379
|
+
interface EmbeddedAttributeFinding {
|
|
2380
|
+
/** The carrier - the element whose over-declared value held the others. */
|
|
2381
|
+
readonly tag: Tag;
|
|
2382
|
+
/** The carrier's VR. Always one of the string VRs; binary VRs are not scanned. */
|
|
2383
|
+
readonly vr: VR;
|
|
2384
|
+
/** The tags of the Data Elements found inside the carrier's value, in wire order. */
|
|
2385
|
+
readonly hidden: readonly Tag[];
|
|
2386
|
+
/** Tag/index chain when the carrier is inside a sequence item; omitted at the root. */
|
|
2387
|
+
readonly contextPath?: readonly string[];
|
|
2388
|
+
}
|
|
2389
|
+
/**
|
|
2390
|
+
* One `SQ` element that was emptied because the parser never materialized its
|
|
2391
|
+
* items, so the de-identifier had no Data Sets to walk.
|
|
2392
|
+
*
|
|
2393
|
+
* PS3.5 2026c §7.5.1 "Item Encoding Rules" states that "Each Item Value shall
|
|
2394
|
+
* contain a DICOM Data Set composed of Data Elements", so an `SQ` element's
|
|
2395
|
+
* value is never opaque bytes - it is Data Elements this run is obliged to
|
|
2396
|
+
* reach. PS3.15 2026c §E.1.1 "De-identifier" states that obligation directly:
|
|
2397
|
+
* an implementation claiming the Basic Application Level Confidentiality
|
|
2398
|
+
* Profile "shall protect or retain all instances of the Attributes listed in
|
|
2399
|
+
* [Table E.1-1], whether contained in the top level Data Set or embedded in an
|
|
2400
|
+
* Item of a Sequence of Items". When the item stream cannot be enumerated the
|
|
2401
|
+
* obligation cannot be discharged element by element, so it falls on the
|
|
2402
|
+
* enclosing attribute - the escalation §E.1.1 itself uses for a SOP Instance
|
|
2403
|
+
* UID inside a Sequence, where "the enclosing Attribute in the top-level Data
|
|
2404
|
+
* Set must be encrypted in its entirety". (That sentence is written about the
|
|
2405
|
+
* encrypt-and-replace mechanism for SOP Instance UIDs, not about Table E.1-1
|
|
2406
|
+
* generally; it is cited here as the standard's own precedent for escalating to
|
|
2407
|
+
* the carrier, not as a rule about this case.)
|
|
2408
|
+
*
|
|
2409
|
+
* Both fields are structural: `tag` is the carrier's and `byteLength` is the
|
|
2410
|
+
* declared size of the value that was dropped. No decoded value appears here,
|
|
2411
|
+
* so this is safe to log.
|
|
2412
|
+
*
|
|
2413
|
+
* The parser always announces the underlying refusal first, on
|
|
2414
|
+
* `Dataset.warnings`: `DICOM_SQ_NOT_DESCENDED` for a defined-length Implicit VR
|
|
2415
|
+
* LE value whose dictionary-resolved `SQ` was not a valid item stream.
|
|
2416
|
+
*
|
|
2417
|
+
* @example
|
|
2418
|
+
* ```ts
|
|
2419
|
+
* import { deidentify, parseDicom } from "@cosyte/dicom";
|
|
2420
|
+
* const { report } = deidentify(parseDicom(buf));
|
|
2421
|
+
* for (const s of report.unauditableSequences) {
|
|
2422
|
+
* console.warn(`${s.tag}: ${String(s.byteLength)} bytes dropped, item stream unreadable`);
|
|
2423
|
+
* }
|
|
2424
|
+
* ```
|
|
2425
|
+
*/
|
|
2426
|
+
interface UnauditableSequenceFinding {
|
|
2427
|
+
/** The `SQ` element that was emptied. */
|
|
2428
|
+
readonly tag: Tag;
|
|
2429
|
+
/** Byte length of the value field that was dropped. Structural, never a value. */
|
|
2430
|
+
readonly byteLength: number;
|
|
2431
|
+
/** Tag/index chain when the carrier is inside a sequence item; omitted at the root. */
|
|
2432
|
+
readonly contextPath?: readonly string[];
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* One element that was emptied because its **on-wire VR is not one of the 34
|
|
2436
|
+
* PS3.5 section 6.2 defines**, so nothing this library did to its bytes counts
|
|
2437
|
+
* as decoding a Value Field.
|
|
2438
|
+
*
|
|
2439
|
+
* ## Why such an element exists at all
|
|
2440
|
+
*
|
|
2441
|
+
* Under an Explicit VR Transfer Syntax the VR is two bytes the sender wrote, and
|
|
2442
|
+
* this parser trusts them (Postel's Law on the read path). The routine way two
|
|
2443
|
+
* arbitrary bytes end up in a VR field is an **under**-declared Value Length
|
|
2444
|
+
* upstream: the reader finishes the short value, and the leftover bytes of the
|
|
2445
|
+
* value that was actually encoded are read as the next Data Element header. Tag,
|
|
2446
|
+
* VR and length are then all fragments of somebody's value, and the element that
|
|
2447
|
+
* genuinely followed is consumed as this fabricated element's "value".
|
|
2448
|
+
*
|
|
2449
|
+
* Measured on `scripts/measure-sq-bound-grid.ts`: a carrier under-declaring by 6
|
|
2450
|
+
* produces `(4156,554C)` with the VR bytes `"E "`, whose value holds the source
|
|
2451
|
+
* `(0010,0020)` Patient ID in full. It reaches **string** carriers exactly as it
|
|
2452
|
+
* reaches binary ones, because the carrier's own VR is not what decides it.
|
|
2453
|
+
*
|
|
2454
|
+
* ## Why emptying, and why it is not a guess
|
|
2455
|
+
*
|
|
2456
|
+
* PS3.5 2026c section 6.2 requires every VR not yet defined to use the long-form
|
|
2457
|
+
* Data Element Structure - "with reserved bytes after the VR and a 32-bit
|
|
2458
|
+
* unsigned integer VL" - so an unrecognized VR read short-form, which is what
|
|
2459
|
+
* this parser does, is by the standard's own structure rule not a reading of a
|
|
2460
|
+
* Value Field. There is nothing to prove about the content: the test is a
|
|
2461
|
+
* membership check against the closed 34-VR set on a field the parser already
|
|
2462
|
+
* recorded, so there is no scan, no per-offset loop, and no cost that follows an
|
|
2463
|
+
* attacker-chosen value length.
|
|
2464
|
+
*
|
|
2465
|
+
* PS3.15 2026c section E.1.1 obliges an implementation claiming the Basic
|
|
2466
|
+
* Application Level Confidentiality Profile to "protect or retain all instances
|
|
2467
|
+
* of the Attributes listed in [Table E.1-1]". Those instances cannot be reached
|
|
2468
|
+
* inside bytes that were never a value, so the obligation falls on the carrier.
|
|
2469
|
+
*
|
|
2470
|
+
* **`UN` is not this.** `UN` is one of the 34, so an ordinary unknown-VR element
|
|
2471
|
+
* - the Implicit VR fallback for a tag this build's dictionary does not publish,
|
|
2472
|
+
* and the CP-246 shape - never reaches here. That is the line the sibling
|
|
2473
|
+
* `SQ`-with-no-items rule could not draw.
|
|
2474
|
+
*
|
|
2475
|
+
* ## Why this finding names no tag, when every sibling finding does
|
|
2476
|
+
*
|
|
2477
|
+
* Because the tag **may be content**, and nothing here can tell. The paragraph
|
|
2478
|
+
* above is the whole argument: when an under-declare desynchronized the reader,
|
|
2479
|
+
* the four tag bytes and the two VR bytes were read out of the middle of some
|
|
2480
|
+
* element's Value Field, so reporting the "tag" would republish four bytes of
|
|
2481
|
+
* the document. An unrecognized VR written honestly, at a correct length, raises
|
|
2482
|
+
* this same code and has an ordinary tag - **and the two are indistinguishable
|
|
2483
|
+
* here**, so the tag is withheld on both routes rather than on a guess. Measured on a synthetic `ST` carrier holding
|
|
2484
|
+
* `"MR BRAIN SMITHSON"`, the fabricated tag is `48544F53` - four bytes of the
|
|
2485
|
+
* surname. {@link EmbeddedAttributeFinding} and
|
|
2486
|
+
* {@link UnauditableSequenceFinding} may carry a tag because theirs came from a
|
|
2487
|
+
* header the sender really wrote; this one may not, and the asymmetry is the
|
|
2488
|
+
* finding rather than an inconsistency.
|
|
2489
|
+
*
|
|
2490
|
+
* `byteOffset` locates the element instead - a position this parser counted.
|
|
2491
|
+
* Nothing here renders a document byte: an offset the parser counted, a decoded
|
|
2492
|
+
* length, and the structural `contextPath`. Safe to log.
|
|
2493
|
+
*
|
|
2494
|
+
* @example
|
|
2495
|
+
* ```ts
|
|
2496
|
+
* import { deidentify, parseDicom } from "@cosyte/dicom";
|
|
2497
|
+
* const { report } = deidentify(parseDicom(buf));
|
|
2498
|
+
* for (const u of report.undefinedVrElements) {
|
|
2499
|
+
* console.warn(`offset ${String(u.byteOffset)}: ${String(u.byteLength)} bytes dropped`);
|
|
2500
|
+
* }
|
|
2501
|
+
* ```
|
|
2502
|
+
*/
|
|
2503
|
+
interface UndefinedVrFinding {
|
|
2504
|
+
/**
|
|
2505
|
+
* Byte offset of the emptied element's header. **This is how the element is
|
|
2506
|
+
* identified, and there is deliberately no `tag` field** - see the note above:
|
|
2507
|
+
* a fabricated header's tag bytes are part of some element's value.
|
|
2508
|
+
*/
|
|
2509
|
+
readonly byteOffset: number;
|
|
2510
|
+
/**
|
|
2511
|
+
* Byte length of the value field that was dropped.
|
|
2512
|
+
*
|
|
2513
|
+
* **An input-derived count, and on this finding specifically that is not a
|
|
2514
|
+
* formality.** For the sibling findings the length came from a header the
|
|
2515
|
+
* sender wrote; here the two length bytes can themselves be value bytes, like
|
|
2516
|
+
* the tag bytes. What is published is the *number* they decode to, never the
|
|
2517
|
+
* bytes - the same footing as every `{n}` in the warning registry, which is
|
|
2518
|
+
* documented there as "an input-derived count". A number is not a rendering,
|
|
2519
|
+
* and the reach is at most a character or so, but do not describe this field
|
|
2520
|
+
* as "structural, never a value" the way its siblings are described.
|
|
2521
|
+
*/
|
|
2522
|
+
readonly byteLength: number;
|
|
2523
|
+
/** Tag/index chain when the carrier is inside a sequence item; omitted at the root. */
|
|
2524
|
+
readonly contextPath?: readonly string[];
|
|
2525
|
+
}
|
|
2351
2526
|
/**
|
|
2352
2527
|
* The audit trail returned alongside the de-identified dataset.
|
|
2353
2528
|
*
|
|
@@ -2372,6 +2547,69 @@ interface DeidentifyReport {
|
|
|
2372
2547
|
readonly attributes: readonly DeidentifiedAttribute[];
|
|
2373
2548
|
/** Private tags removed under the Basic Profile (kept ones are omitted). */
|
|
2374
2549
|
readonly removedPrivateTags: readonly Tag[];
|
|
2550
|
+
/**
|
|
2551
|
+
* Values emptied because whole Data Elements were encoded inside them by an
|
|
2552
|
+
* over-declared Value Length. Empty on a well-formed file; a non-empty array
|
|
2553
|
+
* means the *source* was malformed in a way that hid attributes from the
|
|
2554
|
+
* action table, so treat it as a data-quality alarm on the sender as well as
|
|
2555
|
+
* an audit line. See {@link EmbeddedAttributeFinding}.
|
|
2556
|
+
*/
|
|
2557
|
+
readonly embeddedAttributes: readonly EmbeddedAttributeFinding[];
|
|
2558
|
+
/**
|
|
2559
|
+
* `SQ` elements emptied because the parser did not materialize their items, so
|
|
2560
|
+
* the run had no Data Sets to walk and could not discharge PS3.15 §E.1.1's
|
|
2561
|
+
* obligation inside them. Empty on a well-formed file; a non-empty array means
|
|
2562
|
+
* content was dropped from the de-identified output, and the matching
|
|
2563
|
+
* `DICOM_SQ_NOT_DESCENDED` entry on `Dataset.warnings` says why the parse
|
|
2564
|
+
* refused. See {@link UnauditableSequenceFinding}.
|
|
2565
|
+
*
|
|
2566
|
+
* **Capped, and the cap is on the record only.** A crafted input can carry
|
|
2567
|
+
* tens of thousands of un-auditable elements, so this array (and its matching
|
|
2568
|
+
* warnings) stops at `MAX_UNAUDITABLE_SEQUENCE_FINDINGS`. Every un-auditable
|
|
2569
|
+
* sequence is still emptied; an array exactly that long means "at least this
|
|
2570
|
+
* many", so read it as truncated rather than as a total.
|
|
2571
|
+
*
|
|
2572
|
+
* **It is not a complete list of what went un-audited, either**: a private
|
|
2573
|
+
* `SQ` a {@link Profile} vouches for under `RetainSafePrivate` is kept
|
|
2574
|
+
* verbatim and never appears here.
|
|
2575
|
+
*/
|
|
2576
|
+
readonly unauditableSequences: readonly UnauditableSequenceFinding[];
|
|
2577
|
+
/**
|
|
2578
|
+
* Elements emptied because their on-wire VR is not one of the 34 PS3.5 §6.2
|
|
2579
|
+
* defines, so their bytes are not a Value Field this library decoded and
|
|
2580
|
+
* PS3.15 §E.1.1's obligation over what is inside them could not be discharged.
|
|
2581
|
+
* Empty on a file conformant to **PS3.5 2026c**: a sender that writes one of
|
|
2582
|
+
* the 34 VRs that edition defines never produces one, and an Implicit VR LE
|
|
2583
|
+
* file **cannot** - there the VR comes from the dictionary. The edition is not
|
|
2584
|
+
* pedantry: §6.2 exists precisely to say how a *future* VR will be encoded, so
|
|
2585
|
+
* a file conformant to a later edition using a newly defined VR is the
|
|
2586
|
+
* population that sentence exists for. (What such a file does on this
|
|
2587
|
+
* library's parse path is not summarized here - it was measured and the
|
|
2588
|
+
* shapes disagree.) A non-empty array
|
|
2589
|
+
* means the source desynchronized the reader, usually by under-declaring a
|
|
2590
|
+
* Value Length somewhere earlier. See {@link UndefinedVrFinding}.
|
|
2591
|
+
*
|
|
2592
|
+
* **Capped, and the cap is on the record only**, exactly as
|
|
2593
|
+
* {@link DeidentifyReport.unauditableSequences} is: a crafted 1 MiB input can
|
|
2594
|
+
* carry over a hundred thousand such elements, so this array and its matching
|
|
2595
|
+
* warnings stop at `MAX_UNDEFINED_VR_FINDINGS`. Every one of them is still
|
|
2596
|
+
* emptied; an array exactly that long means "at least this many".
|
|
2597
|
+
*
|
|
2598
|
+
* **A finding here names a byte offset, not a tag** - uniquely among the
|
|
2599
|
+
* report's findings, and for a reason worth reading in
|
|
2600
|
+
* {@link UndefinedVrFinding}: the tag of a fabricated header is itself part of
|
|
2601
|
+
* some element's value.
|
|
2602
|
+
*
|
|
2603
|
+
* Unlike its sibling this list has **no carve-out**, and the reason is
|
|
2604
|
+
* structural rather than a promise: `keepOrEmpty` is the **only** path that
|
|
2605
|
+
* writes a source value into de-identified output unchanged, and the test sits
|
|
2606
|
+
* at the top of it. Every other outcome - `X` remove, `Z`/`C` empty, `D` dummy,
|
|
2607
|
+
* `U` remap, and a private tag the Basic Profile drops - already replaces the
|
|
2608
|
+
* value. So a `RetainSafePrivate` element a {@link Profile} vouches for still
|
|
2609
|
+
* reaches this test and is still emptied, which is where the sibling
|
|
2610
|
+
* `SQ`-with-no-items rule has a real carve-out and this one does not.
|
|
2611
|
+
*/
|
|
2612
|
+
readonly undefinedVrElements: readonly UndefinedVrFinding[];
|
|
2375
2613
|
/**
|
|
2376
2614
|
* Source UID → replacement UID, for cross-file consistency. The **keys are
|
|
2377
2615
|
* document values**, not composed identifiers: this is the one field of the
|
|
@@ -2529,15 +2767,43 @@ declare class DeidentifyError extends Error {
|
|
|
2529
2767
|
* - A private data element kept under `RetainSafePrivate` is kept *verbatim* - if
|
|
2530
2768
|
* it is itself a sequence carrying standard PHI attributes, that nested content
|
|
2531
2769
|
* is not recursed. The profile vouches the element is safe; nest accordingly.
|
|
2532
|
-
* - A
|
|
2533
|
-
*
|
|
2534
|
-
*
|
|
2535
|
-
*
|
|
2536
|
-
*
|
|
2537
|
-
*
|
|
2538
|
-
*
|
|
2539
|
-
*
|
|
2540
|
-
* `
|
|
2770
|
+
* - A **standard** (non-private) `SQ` whose `items` the parser did not
|
|
2771
|
+
* materialize is **emptied**, not kept:
|
|
2772
|
+
* its value is by PS3.5 §7.5.1 a stream of Data Sets, and a run that cannot
|
|
2773
|
+
* enumerate them cannot discharge §E.1.1's obligation inside them, so the
|
|
2774
|
+
* fail-safe answer is to drop the carrier and say so
|
|
2775
|
+
* (`DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE` + `report.unauditableSequences`).
|
|
2776
|
+
* That costs content, and the trade is deliberate: it used to be *kept
|
|
2777
|
+
* verbatim*, which wrote the sender's own un-audited Data Elements - measured,
|
|
2778
|
+
* `(0010,0020)` Patient IDs among them - into output stamped
|
|
2779
|
+
* `(0012,0062) PatientIdentityRemoved = YES`
|
|
2780
|
+
* (`DICOM-DEIDENT-RAWBYTES-PASSTHROUGH`, live through `0.0.6`).
|
|
2781
|
+
* - Two things are still kept verbatim, deliberately. A **private** `SQ`
|
|
2782
|
+
* retained under `RetainSafePrivate` + a {@link Profile}, where the profile has
|
|
2783
|
+
* vouched for the element by creator and tag - the pre-existing "kept
|
|
2784
|
+
* verbatim" limitation above, unchanged. And an undefined-length **`UN`** whose
|
|
2785
|
+
* CP-246 descent was refused: it keeps `vr === "UN"`, and since every ordinary
|
|
2786
|
+
* `UN` element also has no items, the test above cannot be applied there
|
|
2787
|
+
* without emptying every unknown-VR element in every file. That one is
|
|
2788
|
+
* measured and **still leaks** (`PRE-EXISTING`); the reliable consumer-side
|
|
2789
|
+
* test remains `el.items === undefined` on a `UN` element you are trusting a
|
|
2790
|
+
* report about.
|
|
2791
|
+
* - An element whose **on-wire VR is not one of the 34** PS3.5 §6.2 defines is
|
|
2792
|
+
* **emptied**, not kept (`DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE` +
|
|
2793
|
+
* `report.undefinedVrElements`). Such an element is what an *under*-declared
|
|
2794
|
+
* Value Length upstream produces: the reader desynchronizes, reads leftover
|
|
2795
|
+
* value bytes as a Data Element header, and the element that genuinely
|
|
2796
|
+
* followed becomes its "value" - measured carrying a `(0010,0020)` Patient ID
|
|
2797
|
+
* in full. §6.2 requires every undefined VR to be long-form, and this parser
|
|
2798
|
+
* reads it short-form, so those bytes are not a Value Field this library
|
|
2799
|
+
* decoded under any VR. See {@link hasUndefinedVr}. **Unlike the `SQ` rule
|
|
2800
|
+
* above this one has no carve-out** - it sits in `keepOrEmpty`, the only path
|
|
2801
|
+
* that keeps a value verbatim, so `RetainSafePrivate` does not exempt it.
|
|
2802
|
+
* - **Still leaking, measured, and its own decision:** the over-declare swallow
|
|
2803
|
+
* into a **binary** carrier (`OB`/`OW`/`US`/`UN`), 11 grid cells at
|
|
2804
|
+
* `35adc2d`. No content test can decide it - arbitrary bytes are what those
|
|
2805
|
+
* VRs are for - and the one candidate remedy empties conformant binary values.
|
|
2806
|
+
* See `./embedded.ts`.
|
|
2541
2807
|
*
|
|
2542
2808
|
* @module
|
|
2543
2809
|
*/
|
|
@@ -2628,4 +2894,4 @@ interface UidRemapper {
|
|
|
2628
2894
|
*/
|
|
2629
2895
|
declare function makeUidRemapper(root?: string, cache?: Map<string, string>): UidRemapper;
|
|
2630
2896
|
|
|
2631
|
-
export { type AppliedAction, CODING_SCHEME_OIDS, type CodedConcept, DEFAULT_UID_ROOT, DEIDENTIFY_ERROR_CODES, DEIDENTIFY_OPTIONS, Dataset, type DefineProfileOptions, type DeidentifiedAttribute, DeidentifyError, type DeidentifyErrorCode, type DeidentifyOption, type DeidentifyOptions, type DeidentifyReport, type DeidentifyResult, type DicomDate, type DicomDateTime, DicomParseError, type DicomParseWarning, type DicomPosition, DicomSerializeError, type DicomTime, type DicomValue, DicomValueError, index as Dictionary, Element, FATAL_CODES, type FatalCode, type FileMeta, type FileMetaRawElement, type FrameFunctionalGroups, type ImageView, Item, type OnWarningCallback, type OtherPatientId, type ParseOptions, type PatientView, type PersonName, type PersonNameGroup, type PrivateTagDefinition, type Profile, ProfileDefinitionError, type ProfilePrivateTags, type RealWorldValueMap, SERIALIZE_ERROR_CODES, Sequence, type SerializeErrorCode, type SeriesView, type StudyView, type UidRemapper, 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, resolveDecoderLabel, serializeDicom };
|
|
2897
|
+
export { type AppliedAction, CODING_SCHEME_OIDS, type CodedConcept, DEFAULT_UID_ROOT, DEIDENTIFY_ERROR_CODES, DEIDENTIFY_OPTIONS, Dataset, type DefineProfileOptions, type DeidentifiedAttribute, DeidentifyError, type DeidentifyErrorCode, type DeidentifyOption, type DeidentifyOptions, type DeidentifyReport, type DeidentifyResult, type DicomDate, type DicomDateTime, DicomParseError, type DicomParseWarning, type DicomPosition, DicomSerializeError, type DicomTime, type DicomValue, DicomValueError, index as Dictionary, Element, type EmbeddedAttributeFinding, FATAL_CODES, type FatalCode, type FileMeta, type FileMetaRawElement, type FrameFunctionalGroups, type ImageView, Item, type OnWarningCallback, type OtherPatientId, type ParseOptions, type PatientView, type PersonName, type PersonNameGroup, type PrivateTagDefinition, type Profile, ProfileDefinitionError, type ProfilePrivateTags, type RealWorldValueMap, SERIALIZE_ERROR_CODES, Sequence, type SerializeErrorCode, type SeriesView, type StudyView, type UidRemapper, type UnauditableSequenceFinding, type UndefinedVrFinding, 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, resolveDecoderLabel, serializeDicom };
|