@cosyte/dicom 0.0.13 → 0.0.14
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 +178 -0
- package/README.md +5 -3
- package/dist/index.cjs +34 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -22
- package/dist/index.d.ts +81 -22
- package/dist/index.mjs +34 -22
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,183 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0c53142: Test infrastructure: the generator suites prove their pins in a sandbox, so nothing mutates the
|
|
8
|
+
vendored standards while another worker is reading them.
|
|
9
|
+
|
|
10
|
+
**The pin is not relaxed. The mutation is relocated.** Both generator suites prove their vendored
|
|
11
|
+
DocBook pins are preconditions rather than comments, and the only way to prove that is to defeat one:
|
|
12
|
+
repoint `vendor/nema/<part>/SHA.txt` at a mutant document, and, for the check that re-hashes the
|
|
13
|
+
CONTENT rather than the pointer, overwrite the bytes at the pinned path itself. Vitest runs test files
|
|
14
|
+
in parallel, so for as long as a mutation was live every other worker saw it. The documentation
|
|
15
|
+
citation gate re-hashes PS3.5, PS3.6 and PS3.15 at **module load**, which made it a fresh concurrent
|
|
16
|
+
reader of two of the parts those suites mutate, and a reader that throws at module load takes its
|
|
17
|
+
whole file down rather than one case. (Two, not all of them: the gate does not read `part05-2004`,
|
|
18
|
+
which the repeating-groups suite mutates, and nothing mutates `part06`, which the gate does read.)
|
|
19
|
+
The generators now run against a `mkdtemp` copy of `scripts/`, `src/` and `vendor/`, so there is
|
|
20
|
+
nothing for a concurrent reader to observe.
|
|
21
|
+
|
|
22
|
+
**Neither generator took a new input, which is what makes this affordable.** Both resolve their own
|
|
23
|
+
repository root from `import.meta.url`, so relocating the script relocates the document it reads and
|
|
24
|
+
the artifact it writes. The byte-identical regen gate keeps depending on a script with no vendor-root
|
|
25
|
+
argument and no output-path argument; the new `root` option is on the test helper that spawns them,
|
|
26
|
+
not on the scripts. The artifact comparison still reads the **committed**
|
|
27
|
+
`src/dictionary/generated/` file as its baseline, so "regenerates what is committed" still means that.
|
|
28
|
+
|
|
29
|
+
**The window was measured, and so was the fix.** A probe running the citation gate's `readPinned` in
|
|
30
|
+
a loop over all four vendored part directories, against ten runs of the two generator suites, logged
|
|
31
|
+
1,542 read cycles and 945 anomalous observations in four classes: `SHA.txt` holding a non-hash,
|
|
32
|
+
`SHA.txt` naming a directory not on disk, the file at the pinned path not hashing to its pin, and the
|
|
33
|
+
one that matters most, **a pin that verified against a document that is not the committed one**. 742
|
|
34
|
+
of those 945 are on the two parts the citation gate actually reads. A mutant is written into a directory
|
|
35
|
+
named by its own hash, so re-hashing it succeeds: an integrity check cannot see that at all, and the
|
|
36
|
+
reader resolves clauses against a mutated standard and reports green. A SHA pin rewritten mid-run
|
|
37
|
+
means the thing being corrupted is the integrity check itself, which is why teaching a reader to
|
|
38
|
+
tolerate a writer was never on the table. Same probe after the change: 1,437 read cycles, zero
|
|
39
|
+
observations of any class. End to end, looping the reader file against ten runs of the two mutating
|
|
40
|
+
suites: ten of 31 reader runs failed before, zero of 30 after. Both controls were run, because a clean
|
|
41
|
+
after-figure is also what a dead probe prints.
|
|
42
|
+
|
|
43
|
+
**This also closes a flake the repeating-groups suite had disclosed and declined to fix**, and its
|
|
44
|
+
disclosure is deleted rather than reworded: the two generator suites raced each other through
|
|
45
|
+
`src/dictionary/generated/`, which reaches the whole test run and not just those two files, because
|
|
46
|
+
the shipped library imports both artifacts. It was declined then because the obvious fix was an
|
|
47
|
+
output-path override on a script the regen gate depends on. Relocating the tree costs the generators
|
|
48
|
+
no new input at all, so that trade is not the one on the table any more.
|
|
49
|
+
|
|
50
|
+
No library code changed, and no public surface moved.
|
|
51
|
+
|
|
52
|
+
- 023b22d: A diagnostic about a PHI leak is itself a PHI surface: two of the four measured instances are closed,
|
|
53
|
+
and the claim that tied them together is corrected on every surface that carried it.
|
|
54
|
+
|
|
55
|
+
**The three private-tag Tier-2 codes take no tag parameter at all.**
|
|
56
|
+
`DICOM_PRIVATE_TAG_NO_CREATOR`, `DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR` and
|
|
57
|
+
`DICOM_PRIVATE_CREATOR_UNKNOWN` are built from `position` alone. Measured on a synthetic,
|
|
58
|
+
name-bearing payload: an `ST` carrying `"MR BRAIN SMITHSON "` whose Value Length under-declares by 12
|
|
59
|
+
desynchronizes the Implicit VR LE reader onto a fabricated header at an odd group, and the first two
|
|
60
|
+
of those codes rendered its tag as `4E495320` - `"IN S"` in wire order, four letters from inside the
|
|
61
|
+
name - on the same parse, from the same branch of `resolveImplicitVR`. `renderTag` validates a tag's
|
|
62
|
+
shape and therefore cannot refuse a fabricated one, so the bound is the **factory signature**, the
|
|
63
|
+
same remedy `DICOM_NONZERO_RESERVED_BYTES`, `DICOM_ITEM_CROSSES_SEQUENCE_END`,
|
|
64
|
+
`DICOM_DUPLICATE_TAG_IN_DATA_SET` and `DICOM_DUPLICATE_FILE_META_ELEMENT` already take. **What is
|
|
65
|
+
specific here is why it is a bound and not a product call:** all three fire only on an **odd** group,
|
|
66
|
+
and an odd group is the one class of tag no closed table this library holds can vouch for - PS3.6's
|
|
67
|
+
registry is even-group and a `Profile`'s private dictionary is keyed by a creator string this code
|
|
68
|
+
fires because it does not have. The third of the three is bound by that argument rather than by a
|
|
69
|
+
measurement, and is described that way. **The cost, stated:** on a well-formed file with an unclaimed
|
|
70
|
+
private block the tag is no longer in the message. It is still the element's key in the parsed Data
|
|
71
|
+
Set, and `position.byteOffset` locates the header.
|
|
72
|
+
|
|
73
|
+
**`report.embeddedAttributes[].hidden` carries only the tags the run acted on.** The embedded scanner
|
|
74
|
+
listed **every** tag in a run it found inside a kept carrier's Value Field, and a run needs only one
|
|
75
|
+
actionable attribute to be reported - so a fabricated header sitting beside a real one was listed
|
|
76
|
+
too. Measured: a `CS` carrier over-declaring across a fabricated `"SMIT"` header beside a genuine
|
|
77
|
+
`(0010,0020)` reported `hidden: ["4D535449", "00100020"]`. An entry is now one of the **652 literal
|
|
78
|
+
rows** of PS3.15 Table E.1-1 that this run's options left actionable, and **two drafts of that filter
|
|
79
|
+
were refuted before the sentence was true**. Filtering on the resolved Annex E action alone admits
|
|
80
|
+
every odd group, because the Basic Profile removes private attributes as a class. Adding "and an even
|
|
81
|
+
group" still admits every **repeating-group mask hit**: `annexE()` falls through to the family rows,
|
|
82
|
+
and `(50xx,xxxx)` Curve Data leaves the whole 16-bit element number free - 16 groups x 65,536
|
|
83
|
+
elements against 652 literal rows - so `"\fPAR"` composes `500C5241` and returns all four payload
|
|
84
|
+
bytes with one typed read. A mask match proves a rule exists; it does not make the membership finite.
|
|
85
|
+
The shipped test is "the tag has a literal row", which subsumes the odd-group case, since no literal
|
|
86
|
+
row is odd-group. **Two consequences that travel with the field:** it can now be **empty on a real
|
|
87
|
+
finding** (a run whose only actionable members are private attributes, Curve Data or Overlay elements
|
|
88
|
+
names none of them, and the carrier is still emptied and still counted), and it is **still uncapped**.
|
|
89
|
+
`DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED`'s `{n}` is unchanged and still counts the whole run, so
|
|
90
|
+
narrowing `hidden` did not silently re-scope a shipped message; its closing sentence was reworded,
|
|
91
|
+
because it pointed at a field that can now be empty.
|
|
92
|
+
|
|
93
|
+
**`report.removedPrivateTags` is deliberately unchanged.** It is a private-tag field, so no table can
|
|
94
|
+
vouch for its entries and a bound would empty it on every well-formed file - which is what it exists
|
|
95
|
+
to record. That is a product call, and the earlier reading that grouped all three as one call is
|
|
96
|
+
retracted: the test that decides it is whether a closed table can vouch for the tag, not whether the
|
|
97
|
+
tag is real on a good file.
|
|
98
|
+
|
|
99
|
+
**The "safe to log" claim is corrected, and a fifth instance was found correcting it.** Sweeping every
|
|
100
|
+
under-declare delta on both transfer syntaxes for every 4-byte window of the payload rendered as a
|
|
101
|
+
tag, every 4-byte window rendered as a `readUInt32LE` decimal and every 2-byte window rendered as a
|
|
102
|
+
VR, found a third leaking Tier-2 code on the base tree: **`DICOM_ODD_LENGTH_VALUE_PADDED` under
|
|
103
|
+
Explicit VR LE renders a fabricated tag AND a fabricated 32-bit declared length** - eight consecutive
|
|
104
|
+
payload bytes in one message, each reversible with one typed read. It is `PRE-EXISTING`, it is **not
|
|
105
|
+
closed here**, and it is why `ds.warnings[].message` is still not unconditionally safe: that code
|
|
106
|
+
fires on any tag, so the remedy is a membership `renderTag` plus a separate answer for the raw
|
|
107
|
+
length, which is a package-wide decision rather than a rider. It is pinned by an asserted row.
|
|
108
|
+
The claim was also attached to the wrong field: every fixture that produces the leak dies before a
|
|
109
|
+
`Dataset` exists, so the carrier is `onWarning` and the `{ strict: true }` `DicomParseError`, not a
|
|
110
|
+
surviving `ds.warnings` - and that no measured fixture put one on a surviving `ds.warnings` is stated
|
|
111
|
+
as a fact about those fixtures, never as a guarantee. Carriers of the old wording were found by
|
|
112
|
+
folding newlines rather than by a line-based search, since a sentence that wraps is invisible to one:
|
|
113
|
+
`README.md`, `limitations.md`, `troubleshooting.md`, `spec-notes-tolerance.md`, `cookbook.md` and the
|
|
114
|
+
JSDoc on `EmbeddedAttributeFinding`, `DeidentifyReport` and the three factories.
|
|
115
|
+
|
|
116
|
+
**Consumer-visible:** four warning message strings are reworded (no code, no `position` and no
|
|
117
|
+
`err.code` changes), and `embeddedAttributes[].hidden` may be shorter or empty on a file where it was
|
|
118
|
+
populated before. A consumer that string-matched those messages or read `hidden` as the whole run
|
|
119
|
+
should read the code and the warning's count instead.
|
|
120
|
+
|
|
121
|
+
- 3f41849: Docs, cookbook and examples: the roadmap's final phase, plus the gates that keep them honest.
|
|
122
|
+
|
|
123
|
+
**A cookbook that covers the jobs a metadata parser is actually handed.** Four new recipes, every one
|
|
124
|
+
executable in CI: extract metadata and index a folder of studies, build routing keys (hierarchy UIDs,
|
|
125
|
+
Accession Number, Patient ID paired with its issuer), read pixel-interpretation metadata safely, and
|
|
126
|
+
bridge to FHIR `ImagingStudy` and HL7 v2. The FHIR recipe works from the `ImagingStudy` "Mappings for
|
|
127
|
+
DICOM" tab and says which FHIR that is: the URL is the continuous build, not a balloted release, so
|
|
128
|
+
the recipe tells you to pin the mappings page for the FHIR version your integration targets.
|
|
129
|
+
|
|
130
|
+
**A clause citation written with its part beside it is checked against the SHA-pinned normative
|
|
131
|
+
documents, and the gate's coverage is stated rather than rounded up.** A new gate re-hashes
|
|
132
|
+
the vendored PS3.5, PS3.6 and PS3.15 2026c DocBook sources as a precondition, then runs two checks of
|
|
133
|
+
different strength. A clause of a vendored **prose** part (PS3.5, PS3.15) written with its label next
|
|
134
|
+
to its part (`PS3.N §X`, `PS3.N section X`, `PS3.N Annex X`) is resolved by collecting **every**
|
|
135
|
+
candidate section carrying that label and requiring exactly one, so zero and two are both refusals and
|
|
136
|
+
a first-match read cannot take the table of contents. **The gate's coverage is stated rather than
|
|
137
|
+
rounded up to "every citation": a label the text writes away from its part**, a second label in a list
|
|
138
|
+
or a bare `section X` whose part was named a sentence earlier, **is not seen**, and the cookbook says
|
|
139
|
+
so where a reader will meet it. Each clause the text leans
|
|
140
|
+
on for a normative statement is additionally required to carry that sentence **in its own body, not in
|
|
141
|
+
a subsection** (a body that swallowed subsections would certify `§7.5` for a `§7.5.2` fact and `§E.1`
|
|
142
|
+
for an `§E.1.1` one, which are the two confusions this package has already paid for; both are pinned
|
|
143
|
+
as controls). PS3.6 is cited for attribute identity rather than for prose, and is checked by its own
|
|
144
|
+
case: every `(gggg,eeee) Some Name` pair written anywhere in the docs must be the registry's own name
|
|
145
|
+
for that tag. **A numbered clause of a part this repository does not vendor may no longer be cited at
|
|
146
|
+
all**: two such citations were in the README and are replaced by prose that names the part and says
|
|
147
|
+
what is and is not claimed.
|
|
148
|
+
|
|
149
|
+
**`@example` on every public export is a gate rather than a convention.** Two exports were missing one
|
|
150
|
+
and now have it. The checker walks the public barrel through the compiler, so a namespace export and a
|
|
151
|
+
type count the same as a function does, and it carries a mutation control that proves it can go red.
|
|
152
|
+
|
|
153
|
+
**The PHI scanner reads doc fixtures.** The documentation ships DICOM objects as base64-encoded Part 10
|
|
154
|
+
buffers inline in markdown, and until now the scanner never opened one: to a text sweep a base64 run is
|
|
155
|
+
a single alphanumeric token with no `FAMILY^GIVEN` and no `YYYYMMDD` in it. `README.md` and
|
|
156
|
+
`docs-content/**` are now a second corpus, embedded objects are decoded and walked as DICOM, and both
|
|
157
|
+
the preamble-bearing and the preamble-less shape are recognized, the latter being what the cookbook
|
|
158
|
+
ships to demonstrate `DICOM_MISSING_PREAMBLE`. **The run floor is not the filter, and a first draft
|
|
159
|
+
that treated it as one shipped blind**: it required 120 base64 characters on the reasoning that a Part
|
|
160
|
+
10 object is big, and the cookbook's preamble-less fixture encodes to 88, so the one file the route's
|
|
161
|
+
own comments named as its reason was the one file it never opened while every test still passed. The
|
|
162
|
+
floor is now the shortest run that could encode a single Data Element header, the decode does the
|
|
163
|
+
filtering, and a regression case takes the **shortest real object out of the shipped cookbook** rather
|
|
164
|
+
than building its own. **It found real content the moment it ran**: every
|
|
165
|
+
sample object on the site carried a Study Date inside the 120-year window. Each is now `19000101`, and
|
|
166
|
+
the docs say why rather than leaving it as an unexplained oddity.
|
|
167
|
+
|
|
168
|
+
**A prominent "do not over-trust" page.** Known limitations moves out of the end of a long
|
|
169
|
+
troubleshooting page and into its own entry near the top of the navigation, linked from the README's
|
|
170
|
+
opening and from Getting started. It is an index rather than a second copy: scope non-goals, the open
|
|
171
|
+
PHI residuals (the retain-route leak whose extent is a matrix, the over-long `LO` in `(0012,0063)` that
|
|
172
|
+
this library is the likeliest writer of, `contextPath` being inert and not safe to log), the structural
|
|
173
|
+
facts no reader can resolve, and what is deliberately never defaulted.
|
|
174
|
+
|
|
175
|
+
**Two owed corrections.** The README described a Tier-2 warning message as "never composed from the
|
|
176
|
+
document", and `ParseOptions.strict`'s JSDoc called one "safe to log whole". Neither is true without a
|
|
177
|
+
qualifier: the registry's tag slot is filled by a shape check, which cannot refuse a tag a lying Value
|
|
178
|
+
Length composed out of somebody's value. Both now say safe on a well-formed file and not
|
|
179
|
+
unconditionally safe, which is what the measurement supports.
|
|
180
|
+
|
|
3
181
|
## 0.0.13
|
|
4
182
|
|
|
5
183
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
A developer-focused DICOM Part 10 parser and utility library for Node.js and TypeScript. **Metadata-first**: it reads the headers (patient, study, series, image, codes, UIDs) leniently and fast, exposes pixel data as raw bytes, and **never decodes pixels**. Sibling to [`@cosyte/hl7`](https://github.com/cosyte/hl7); same engineering bar.
|
|
18
18
|
|
|
19
|
+
> **Before you point this at real data, read [Known limitations](#known-limitations--non-goals).** Mis-reading a patient identifier, an image's signedness, or a rescale slope can cause real clinical harm, so the boundary is a deliverable here rather than a footnote: what is out of scope, which PHI residuals are measured-and-open, and which fields are not safe to log. A de-identified output from this package is **metadata-de-identified only**.
|
|
20
|
+
|
|
19
21
|
---
|
|
20
22
|
|
|
21
23
|
## Quickstart
|
|
@@ -157,7 +159,7 @@ A quirky object is tolerated rather than rejected, and absent fields come back `
|
|
|
157
159
|
|
|
158
160
|
### Build routing keys
|
|
159
161
|
|
|
160
|
-
Routing and reconciliation hang off a small set of identifiers. Surface them correctly: a Patient ID without its issuer is ambiguous across systems (PS3.3
|
|
162
|
+
Routing and reconciliation hang off a small set of identifiers. Surface them correctly: a Patient ID without its issuer is ambiguous across systems. The two attributes are `(0010,0020)` Patient ID and `(0010,0021)` Issuer of Patient ID in the PS3.6 2026c registry, which is vendored and SHA-pinned here; the module that requires them to be read together is in PS3.3, which is **not** vendored here, so no clause number is claimed for it.
|
|
161
163
|
|
|
162
164
|
```ts
|
|
163
165
|
// Hierarchy keys for filing into Study → Series → Instance:
|
|
@@ -187,7 +189,7 @@ img.pixelSpacing; // (0028,0030) patient-plane mm, distinct from imagerPixelSpac
|
|
|
187
189
|
|
|
188
190
|
> **Vendor note.** Philips writes private rescale tags `(2005,1409/140A/140B)` that shadow the standard `(0028,1052/1053)`; using the standard tags alone can yield non-quantitative values. This parser **preserves** the private tags so you can prefer them. Reach them with `ds.get("20051409")` (optionally under `profiles.philips`).
|
|
189
191
|
|
|
190
|
-
For Enhanced multi-frame objects, `image.frame(i)` resolves each frame's functional-group macros Per-Frame
|
|
192
|
+
For Enhanced multi-frame objects, `image.frame(i)` resolves each frame's functional-group macros Per-Frame first and Shared second. The functional-group macros are defined in PS3.3, which is **not** vendored here, so no clause number is claimed for them. It throws a `DicomValueError` (carrying only structural facts, never PHI) for an out-of-range frame or a required geometry macro missing from both groups.
|
|
191
193
|
|
|
192
194
|
```ts
|
|
193
195
|
if (img.isEnhancedMultiFrame) {
|
|
@@ -307,7 +309,7 @@ At an RSNA-era interoperability test, ~80% of real-world patient CDs failed stri
|
|
|
307
309
|
| 2 | Warning | Recoverable deviation | `DICOM_MISSING_PREAMBLE` |
|
|
308
310
|
| 3 | Fatal (always) | Unrecoverable structural error | `NOT_DICOM_PART_10` |
|
|
309
311
|
|
|
310
|
-
Tier-2 warnings are plain data on `ds.warnings`. Each carries a stable string `code`, a `message` looked up from a frozen registry
|
|
312
|
+
Tier-2 warnings are plain data on `ds.warnings`. Each carries a stable string `code`, a `message` looked up from a frozen registry, and a `position` with the byte offset where it occurred, so you can react programmatically. **The message is safe to log on a well-formed file and is not unconditionally safe**: the registry's `{tag}` slot is filled by a shape check, and a shape check cannot refuse a tag that a lying Value Length composed out of somebody's value. Measured: an `ST` carrying a name whose Value Length under-declares desynchronizes the Explicit VR LE reader onto a fabricated header whose declared length is odd, and `DICOM_ODD_LENGTH_VALUE_PADDED` renders four bytes of that payload as the tag and four more as the decimal length. It is disclosed rather than guarded, because that code fires on any tag and withholding it would take the tag off every element that pads an odd length in every real-world file. **Every fixture that reaches it dies before a `Dataset` exists**, so the message travels on `onWarning` or on the `{ strict: true }` error rather than on a surviving `ds.warnings` - a fact about those fixtures, not a promise about the parser. The three **private-tag** codes take no tag parameter at all: an odd group is the one class of tag no closed table here can vouch for. `w.code` and `w.position` carry nothing from the document. If you log messages from untrusted files verbatim, treat a tag or a length in one as document-derived.
|
|
311
313
|
|
|
312
314
|
```ts
|
|
313
315
|
import { parseDicom, WARNING_CODES } from "@cosyte/dicom";
|
package/dist/index.cjs
CHANGED
|
@@ -11,7 +11,7 @@ var __export = (target, all) => {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
// src/version.ts
|
|
14
|
-
var VERSION = "0.0.
|
|
14
|
+
var VERSION = "0.0.14";
|
|
15
15
|
|
|
16
16
|
// src/dictionary/index.ts
|
|
17
17
|
var dictionary_exports = {};
|
|
@@ -12208,7 +12208,11 @@ var WARNING_MESSAGES = Object.freeze({
|
|
|
12208
12208
|
DICOM_UNDEFINED_LENGTH_IN_EXPLICIT_VR: "Element ({tag}) uses undefined length (0xFFFFFFFF) under an Explicit VR transfer syntax.",
|
|
12209
12209
|
DICOM_ODD_LENGTH_VALUE_PADDED: "Element ({tag}) has odd declared length {n}; cursor advanced by one padding byte to maintain alignment.",
|
|
12210
12210
|
DICOM_VR_MISMATCH: "Element ({tag}) on-wire VR is {vr2}; dictionary lists {vr}. Trusting on-wire VR.",
|
|
12211
|
-
|
|
12211
|
+
// No tag, and the reason is the one `nonzeroReservedBytes` states: this code's
|
|
12212
|
+
// three siblings below fire only on an ODD group, and an odd group is the one
|
|
12213
|
+
// class of tag no closed table this library holds can ever vouch for. See
|
|
12214
|
+
// `privateTagNoCreator`.
|
|
12215
|
+
DICOM_PRIVATE_TAG_NO_CREATOR: "A private element has no Private Creator registered for its block; treating as VR=UN. Its tag is withheld; the byte offset identifies the element.",
|
|
12212
12216
|
DICOM_GROUP_LENGTH_IN_DATASET: "Retired Group Length element ({tag}) encountered in dataset; preserved as-is.",
|
|
12213
12217
|
// The tag is deliberately absent, and this is the only *parser* message where
|
|
12214
12218
|
// that is true. See `nonzeroReservedBytes`. Kept terse for the same reason the
|
|
@@ -12254,10 +12258,10 @@ var WARNING_MESSAGES = Object.freeze({
|
|
|
12254
12258
|
// `endLimit < buffer.length` conjunct bounds it by the buffer.
|
|
12255
12259
|
DICOM_ITEM_CROSSES_SEQUENCE_END: "Item ({tag}) declares a length reaching past its enclosing sequence's declared end, so it reads the enclosing Data Set's bytes; {n2} bytes remained inside the sequence. The file's two length fields disagree (PS3.5 7.5.1 and 7.5.2 govern them); the item's is used. The declared length is withheld; the byte offset locates the item.",
|
|
12256
12260
|
DICOM_PIXEL_DATA_LENGTH_MISMATCH: "(7FE0,0010) PixelData declared length {n} does not match computed {n2} bytes.",
|
|
12257
|
-
DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR: "
|
|
12258
|
-
DICOM_PRIVATE_CREATOR_UNKNOWN: "
|
|
12261
|
+
DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR: "A private element under Implicit VR LE has no VR override; falling back to UN. Its tag is withheld; the byte offset identifies the element.",
|
|
12262
|
+
DICOM_PRIVATE_CREATOR_UNKNOWN: "A private element has a Private Creator the active profile's private dictionary does not name; falling back to UN. The creator string is not reproduced here - read the (gggg,00EE) element if you need it. Its tag is withheld; the byte offset identifies the element.",
|
|
12259
12263
|
DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED: "Pixel Data is present and Burned In Annotation is not 'NO'; this metadata-only de-identifier cannot inspect or clean pixels. Recognizable text may remain burned into the image.",
|
|
12260
|
-
DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED: "Element ({tag}) {vr} was kept by the action table, but its value ends with {n} whole Data Element(s) - an over-declared Value Length swallowed what followed it. Emptied rather than kept, because the action table cannot see an attribute encoded inside a value.
|
|
12264
|
+
DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED: "Element ({tag}) {vr} was kept by the action table, but its value ends with {n} whole Data Element(s) - an over-declared Value Length swallowed what followed it. Emptied rather than kept, because the action table cannot see an attribute encoded inside a value. report.embeddedAttributes names the ones this run acts on that also have a literal Table E.1-1 row, which may be none of them.",
|
|
12261
12265
|
// Deliberately short. One of these is raised per un-auditable element, so a
|
|
12262
12266
|
// long message is multiplied by an element count the input controls; the
|
|
12263
12267
|
// reasoning belongs in the docs, not in a string repeated thousands of times.
|
|
@@ -12320,8 +12324,8 @@ function oddLengthValuePadded(position, tag, declaredLength) {
|
|
|
12320
12324
|
function vrMismatch(position, tag, dictVR, fileVR) {
|
|
12321
12325
|
return build2(WARNING_CODES.DICOM_VR_MISMATCH, position, { tag, vr: dictVR, vr2: fileVR });
|
|
12322
12326
|
}
|
|
12323
|
-
function privateTagNoCreator(position
|
|
12324
|
-
return build2(WARNING_CODES.DICOM_PRIVATE_TAG_NO_CREATOR, position
|
|
12327
|
+
function privateTagNoCreator(position) {
|
|
12328
|
+
return build2(WARNING_CODES.DICOM_PRIVATE_TAG_NO_CREATOR, position);
|
|
12325
12329
|
}
|
|
12326
12330
|
function groupLengthInDataset(position, tag) {
|
|
12327
12331
|
return build2(WARNING_CODES.DICOM_GROUP_LENGTH_IN_DATASET, position, { tag });
|
|
@@ -12353,11 +12357,11 @@ function duplicateTagInDataSet(position) {
|
|
|
12353
12357
|
function duplicateFileMetaElement(position) {
|
|
12354
12358
|
return build2(WARNING_CODES.DICOM_DUPLICATE_FILE_META_ELEMENT, position);
|
|
12355
12359
|
}
|
|
12356
|
-
function implicitVRForPrivateTagWithoutVR(position
|
|
12357
|
-
return build2(WARNING_CODES.DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR, position
|
|
12360
|
+
function implicitVRForPrivateTagWithoutVR(position) {
|
|
12361
|
+
return build2(WARNING_CODES.DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR, position);
|
|
12358
12362
|
}
|
|
12359
|
-
function privateCreatorUnknown(position
|
|
12360
|
-
return build2(WARNING_CODES.DICOM_PRIVATE_CREATOR_UNKNOWN, position
|
|
12363
|
+
function privateCreatorUnknown(position) {
|
|
12364
|
+
return build2(WARNING_CODES.DICOM_PRIVATE_CREATOR_UNKNOWN, position);
|
|
12361
12365
|
}
|
|
12362
12366
|
function burnedInAnnotationNotRemoved(position) {
|
|
12363
12367
|
return build2(WARNING_CODES.DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED, position);
|
|
@@ -12462,18 +12466,18 @@ function resolveImplicitVR(tag, ctx, emit, position) {
|
|
|
12462
12466
|
}
|
|
12463
12467
|
const creator = resolvePrivateCreator(tag, ctx);
|
|
12464
12468
|
if (creator === void 0) {
|
|
12465
|
-
emit(privateTagNoCreator(position
|
|
12466
|
-
emit(implicitVRForPrivateTagWithoutVR(position
|
|
12469
|
+
emit(privateTagNoCreator(position));
|
|
12470
|
+
emit(implicitVRForPrivateTagWithoutVR(position));
|
|
12467
12471
|
return "UN";
|
|
12468
12472
|
}
|
|
12469
12473
|
if (ctx.profile !== void 0) {
|
|
12470
12474
|
const def = resolvePrivateTag(ctx.profile, tag, creator);
|
|
12471
12475
|
if (def !== void 0) return def.vr;
|
|
12472
12476
|
if (!ctx.profile.privateDictionary.has(creator)) {
|
|
12473
|
-
emit(privateCreatorUnknown(position
|
|
12477
|
+
emit(privateCreatorUnknown(position));
|
|
12474
12478
|
}
|
|
12475
12479
|
}
|
|
12476
|
-
emit(implicitVRForPrivateTagWithoutVR(position
|
|
12480
|
+
emit(implicitVRForPrivateTagWithoutVR(position));
|
|
12477
12481
|
return "UN";
|
|
12478
12482
|
}
|
|
12479
12483
|
const direct = TAGS[tag];
|
|
@@ -15225,6 +15229,10 @@ function hasByteOutsideRepertoire(region, carrierVr) {
|
|
|
15225
15229
|
}
|
|
15226
15230
|
return false;
|
|
15227
15231
|
}
|
|
15232
|
+
function isTableBound(tag) {
|
|
15233
|
+
const action = annexE(tag);
|
|
15234
|
+
return action !== void 0 && action.repeatingGroup === void 0;
|
|
15235
|
+
}
|
|
15228
15236
|
function findEmbeddedAttributes(value, carrierVr, encoding, isActionable) {
|
|
15229
15237
|
if (!isScannableCarrier(carrierVr)) return void 0;
|
|
15230
15238
|
if (value.length < MIN_HEADER) return void 0;
|
|
@@ -15248,15 +15256,19 @@ function findEmbeddedAttributes(value, carrierVr, encoding, isActionable) {
|
|
|
15248
15256
|
for (let off = 0; off + MIN_HEADER <= n; off += 2) {
|
|
15249
15257
|
if (hit[off >> 1] !== 1) continue;
|
|
15250
15258
|
if (!hasByteOutsideRepertoire(window.subarray(off), carrierVr)) return void 0;
|
|
15251
|
-
const
|
|
15259
|
+
const hidden = [];
|
|
15260
|
+
let elementCount = 0;
|
|
15252
15261
|
let pos = off;
|
|
15253
15262
|
while (pos < n) {
|
|
15254
15263
|
const decoded = decodeAt(window, pos, encoding);
|
|
15255
15264
|
if (decoded === void 0) break;
|
|
15256
|
-
if (decoded.tag !== void 0)
|
|
15265
|
+
if (decoded.tag !== void 0) {
|
|
15266
|
+
elementCount += 1;
|
|
15267
|
+
if (isActionable(decoded.tag) && isTableBound(decoded.tag)) hidden.push(decoded.tag);
|
|
15268
|
+
}
|
|
15257
15269
|
pos = decoded.next;
|
|
15258
15270
|
}
|
|
15259
|
-
return
|
|
15271
|
+
return { hidden, elementCount };
|
|
15260
15272
|
}
|
|
15261
15273
|
return void 0;
|
|
15262
15274
|
}
|
|
@@ -15461,8 +15473,8 @@ function keepOrEmpty(el, ctx, contextPath, out) {
|
|
|
15461
15473
|
emptyUndefinedVrElement(el, ctx, contextPath, out);
|
|
15462
15474
|
return false;
|
|
15463
15475
|
}
|
|
15464
|
-
const
|
|
15465
|
-
if (
|
|
15476
|
+
const run = findEmbeddedAttributes(el.rawBytes, el.vr, ctx.encoding, (t) => actsOnTag(t, ctx));
|
|
15477
|
+
if (run === void 0) {
|
|
15466
15478
|
out.elements.set(el.tag, el);
|
|
15467
15479
|
return true;
|
|
15468
15480
|
}
|
|
@@ -15470,11 +15482,11 @@ function keepOrEmpty(el, ctx, contextPath, out) {
|
|
|
15470
15482
|
out.embeddedAttributes.push({
|
|
15471
15483
|
tag: el.tag,
|
|
15472
15484
|
vr: el.vr,
|
|
15473
|
-
hidden,
|
|
15485
|
+
hidden: run.hidden,
|
|
15474
15486
|
...contextPath.length > 0 ? { contextPath: [...contextPath] } : {}
|
|
15475
15487
|
});
|
|
15476
15488
|
out.warnings.push(
|
|
15477
|
-
embeddedAttributeRemoved({ byteOffset: el.byteOffset }, el.tag, el.vr,
|
|
15489
|
+
embeddedAttributeRemoved({ byteOffset: el.byteOffset }, el.tag, el.vr, run.elementCount)
|
|
15478
15490
|
);
|
|
15479
15491
|
return false;
|
|
15480
15492
|
}
|