@cosyte/dicom 0.0.1 → 0.0.3
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 +161 -25
- package/README.md +53 -48
- package/TRADEMARKS.md +32 -0
- package/dist/index.cjs +11 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +133 -133
- package/dist/index.d.ts +133 -133
- package/dist/index.mjs +11 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,139 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Em-dash brand gate in CI (`EMDASH-CONFORMANCE`).** The founder directive of 2026-07-24
|
|
10
|
+
(`knowledgebase/06-brand/voice-and-tone.md`, "No em dashes. Ever.") bans `U+2014` outright across
|
|
11
|
+
every cosyte surface and names commit messages explicitly, and the meta-repo's
|
|
12
|
+
`documentation/conventions.md` has stated for weeks that the rule is CI-gated. It now actually is,
|
|
13
|
+
here: `scripts/check-no-emdash.sh` (`pnpm check:no-emdash`) plus a dedicated
|
|
14
|
+
`.github/workflows/no-emdash.yml` job that scans **both** halves the rule covers, the tracked files
|
|
15
|
+
**and** the PR title, body, and commit messages. The workflow carries the non-default `edited`
|
|
16
|
+
pull-request trigger, which is load-bearing: this repo squash-merges under
|
|
17
|
+
`squash_merge_commit_title: COMMIT_OR_PR_TITLE` and `squash_merge_commit_message: COMMIT_MESSAGES`
|
|
18
|
+
(read off the repo, not assumed), so the subject that lands on `main` is the PR title, or the lone
|
|
19
|
+
commit's subject when the branch has exactly one, and the body is the branch commit messages.
|
|
20
|
+
Without `edited` a title changed after the last push would never be re-checked. The PR body does
|
|
21
|
+
**not** land; the gate scans it anyway, as deliberate over-strictness on a surface that costs
|
|
22
|
+
nothing to cover. It is a separate workflow rather than a job in `ci.yml` because that trigger
|
|
23
|
+
would otherwise re-run the whole Node 22 + 24 matrix on every typo fix, and because the shared
|
|
24
|
+
`cosyte/.github` pipeline runs no arbitrary repo script.
|
|
25
|
+
**This one did change content, unlike the earlier ports.** An ecosystem survey had measured
|
|
26
|
+
markdown only (0 of 25 `.md` files) and read dicom as already clean. Measuring all 178 tracked
|
|
27
|
+
files found six live em dashes in four non-markdown files, all removed here (see Changed, below).
|
|
28
|
+
Measure every tracked file, because that is what the scan covers.
|
|
29
|
+
The script is the **text-only** variant, taken from `ncpdp` (PR #34, `39212bb`) rather than from
|
|
30
|
+
the older `knowledgebase` copy, so it carries `ncpdp`'s two fixes to the shared shape instead of
|
|
31
|
+
inheriting the holes: a tracked file named `-` was read by `grep` as standard input (which `xargs`
|
|
32
|
+
points at `/dev/null`) and so was never opened, and `-d skip` silently passed a tracked symlink to
|
|
33
|
+
a directory. Both are re-proved here rather than taken on faith: with the `./` path prefix removed
|
|
34
|
+
the gate prints OK and exits 0 over a live em dash in a file named `-`, and with `-d skip` restored
|
|
35
|
+
it goes green over an unread symlink.
|
|
36
|
+
It deliberately omits `grep -I`, and in this repo that is the most load-bearing flag choice in the
|
|
37
|
+
file. `src/dataset/vr/charset.ts` holds a functional NUL byte inside `/[\x00 ]+$/u`, the regex that
|
|
38
|
+
strips DICOM's own padding, so grep classifies it binary. It carries no em dash today and scans
|
|
39
|
+
green, which is the whole answer to the belief that this port was blocked: the red would come from
|
|
40
|
+
a match, never from the NUL. **If it ever gains one, the gate reds.** Measured with GNU grep 3.8:
|
|
41
|
+
grep exits 0, writes nothing to stdout, and writes `binary file matches` to stderr, so the hit
|
|
42
|
+
never reaches the hit list and the stderr capture fails the run instead. With `-I` added, the same
|
|
43
|
+
edit goes green, which is why `-I` stays out. The failure message names that case explicitly rather
|
|
44
|
+
than blaming an I/O error that did not happen.
|
|
45
|
+
A gate that prints OK when it did not read its input is worse than no gate, so nine routes by which
|
|
46
|
+
a dead or blind scan could still report green are each checked red, not assumed: a corrupt git
|
|
47
|
+
index, an unreadable tracked file, a tracked file named `-q`, a C-quoted non-ASCII path, a
|
|
48
|
+
mis-encoded text file, an empty tracked-file list, a tracked file named `-`, a tracked symlink to a
|
|
49
|
+
directory, and the NUL-bearing source file gaining an em dash.
|
|
50
|
+
Known limits are documented in the script header and inherited knowingly from the shared shape
|
|
51
|
+
rather than patched in this copy alone: encoded-form matching is literal (so `—` without the
|
|
52
|
+
semicolon, and lowercase `%e2%80%94`, pass), stderr capture binds to the scanning `grep` rather
|
|
53
|
+
than to the filters ahead of it, an em dash encoded in a non-UTF-8 charset is not matched (a live
|
|
54
|
+
possibility here, given `(0008,0005)` Specific Character Set fixtures), and the scan reads file
|
|
55
|
+
**contents** only, so a tracked path that itself carries an em dash passes. Tooling only: no
|
|
56
|
+
runtime, public-API, or parse-behavior change.
|
|
57
|
+
- **`docs-content/` now covers the full canonical Diátaxis spine** (`DOCS-CONTENT-P6`). Beyond the
|
|
58
|
+
existing Overview (`intro`), the sidebar gains **Installation** and **Quickstart** (tutorials),
|
|
59
|
+
five **Core Concepts** notes (the object model, the tolerance/warning model, the typed value
|
|
60
|
+
layer, the safety-critical views, and the source-profile system), a **Guides** cookbook (four
|
|
61
|
+
recipes: re-serialize, de-identify, read raw pixel data, triage warnings), and a
|
|
62
|
+
**Troubleshooting & known limitations** reference. Every documented capability is grounded in the
|
|
63
|
+
package's actually-shipped surface; the metadata-first boundary is stated explicitly, with the
|
|
64
|
+
permanent non-goals named in Troubleshooting (**no pixel decode, no DIMSE networking, no
|
|
65
|
+
DICOMweb**, no pixel-level de-identification).
|
|
66
|
+
- **Doc/code-agreement gate (`test/docs-content.test.ts`).** Every ` ```ts runnable ` snippet in
|
|
67
|
+
`docs-content/` is extracted, compiled, and executed against the **built** package via
|
|
68
|
+
`docSnippetSuite()` from `@cosyte/vitest-config/snippets`, with its inline `// =>` assertions
|
|
69
|
+
checked, so a documented example can never silently drift from the code. All examples use
|
|
70
|
+
synthetic, base64-encoded Part 10 objects (invented patient, fake UIDs); no real PHI, no `.dcm`
|
|
71
|
+
file on disk.
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
|
|
75
|
+
- **Removed the six em dashes the new gate found (`EMDASH-CONFORMANCE`).** Four tracked files, none
|
|
76
|
+
of them markdown: `.github/CODEOWNERS` (2), `.github/workflows/release.yml` (2),
|
|
77
|
+
`vendor/nema/SHA.txt` (1), and the npm `description` in `package.json` (1). The last is the only
|
|
78
|
+
one visible outside the repo: the package description on npm and on the GitHub sidebar now reads
|
|
79
|
+
"Developer-focused DICOM Part 10 parser + utility library for Node.js and TypeScript:
|
|
80
|
+
metadata-first, vendor-quirky-tolerant, dual ESM/CJS." Each rewrite replaces the character with a
|
|
81
|
+
period, colon, or comma, per the rule's own instruction; none re-encodes it, and no wording beyond
|
|
82
|
+
the punctuation changed.
|
|
83
|
+
- **Bumped the `@cosyte/vitest-config` devDependency to `^0.0.2`** to pick up the `./snippets`
|
|
84
|
+
export that ships the doc/code-agreement runner.
|
|
85
|
+
- **Corrected the element-access examples in `intro.md`.** `Dataset.get` / `has` take the
|
|
86
|
+
8-character `(group,element)` **tag** form only. The prior snippets showed `get("PatientName")`,
|
|
87
|
+
`get("(0010,0010)")`, and `get("StudyDate")`, all of which return `undefined`. They now use the
|
|
88
|
+
tag form (and show resolving a keyword to its tag via `Dictionary.byKeyword`), matching the code.
|
|
89
|
+
|
|
90
|
+
- **All tests now live in a top-level `test/` mirroring `src/`.** `@cosyte/dicom` was the lone parser
|
|
91
|
+
that co-located `*.test.ts` files beside their source; the 32 co-located suites were moved to
|
|
92
|
+
`test/<path>/` preserving the `src/` sub-structure, bringing the repo in line with the
|
|
93
|
+
`hl7`/`mllp`/`x12`/`ccda`/`ncpdp` archetype (`DICOM-TEST-RELOCATE`). Relative imports in the moved
|
|
94
|
+
files were retargeted (`./foo.js` → `../../src/<path>/foo.js`). `vitest.config.ts` drops the
|
|
95
|
+
`src/**/*.test.ts` include glob, so the config now matches the archetype and a stray co-located
|
|
96
|
+
test is no longer silently collected alongside the `test/` suite. Test-only, no public-surface
|
|
97
|
+
change: same 585 passing tests (+1 todo), coverage gates (still pointed at the `src/` source
|
|
98
|
+
directories) unchanged and green.
|
|
99
|
+
|
|
7
100
|
### Fixed
|
|
8
101
|
|
|
9
|
-
- **`
|
|
102
|
+
- **`Dictionary Regen` is green again (`DICOM-DICT-REGEN`).** The byte-identical regen gate had been
|
|
103
|
+
red since 2026-06-24: seven consecutive failing runs, four of them pushes to `main`, every one of
|
|
104
|
+
them failing in its first real step. The last green run was 2026-05-04.
|
|
105
|
+
`.github/workflows/dictionary-regen.yml` passed `version: 10` to `pnpm/action-setup@v6` while
|
|
106
|
+
`package.json` already declares `packageManager: pnpm@10.0.0`, and v6 treats two sources of truth as
|
|
107
|
+
a hard error: "Multiple versions of pnpm specified". The job died in about 13 seconds having never
|
|
108
|
+
installed anything, never run `pnpm gen:all`, and never compared a single generated byte. Dropping
|
|
109
|
+
the redundant `version:` input lets `packageManager` win, matching the fix `astm` already applied to
|
|
110
|
+
its fuzz workflow (`44c3d88`).
|
|
111
|
+
Two smaller repairs to the same file, both aimed at the gate actually asserting something rather
|
|
112
|
+
than merely starting. The workflow now lists **itself** in its `paths` filters, so a change to the
|
|
113
|
+
gate re-runs the gate; without that, the PR fixing a broken gate could not demonstrate the gate
|
|
114
|
+
fixed. And the verify step now also fails on **untracked** files under `src/dictionary/generated/`:
|
|
115
|
+
`git diff` reports modified tracked files only, so a generator that began emitting a brand-new
|
|
116
|
+
artifact would have left it untracked and invisible to the check.
|
|
117
|
+
The committed dictionary itself is **not** drifted. Regenerating from the pinned inputs
|
|
118
|
+
(`vendor/innolitics/90571bc`) reproduces all four artifacts byte for byte: 5,129 tags, 5,035
|
|
119
|
+
keywords, 268 UIDs, 617 Annex E entries, zero diff. Verified as a positive control rather than
|
|
120
|
+
assumed: perturbing a VR in the vendor input made the gate red, and restoring it made the gate
|
|
121
|
+
green, so the check reads its input. Nothing in the tag tables needed correcting, and nothing
|
|
122
|
+
regenerated was committed.
|
|
123
|
+
|
|
124
|
+
- **Corrected the `private: true` claim in `.github/workflows/release.yml`.** A comment at the top of
|
|
125
|
+
the release caller asserted that `@cosyte/dicom` is `private: true` and that `changeset publish` is
|
|
126
|
+
therefore "a no-op until that flag is removed". `package.json` carries no `private` field, the repo
|
|
127
|
+
is public, and the package ships on npm at `0.0.1`, so the comment described the release pipeline as
|
|
128
|
+
inert when it is live. Documentation only, no behavior change, but a misleading note on a workflow
|
|
129
|
+
that really does publish is worth more than a stale one elsewhere.
|
|
130
|
+
|
|
131
|
+
- **Corrected stale publish-status language in `docs-content/` (`README-ORG-SWEEP`).**
|
|
132
|
+
`installation.md` claimed the package was "not yet published to npm" and that the install command
|
|
133
|
+
was only "the shape it will take at first publish"; `troubleshooting.md` listed a "Not yet
|
|
134
|
+
published … not on npm; the first provenance publish is gated on the coordinated public launch"
|
|
135
|
+
non-goal. `@cosyte/dicom` is published on npm at `0.0.1` and public. Both now state the truth
|
|
136
|
+
(published, public, still pre-alpha on the `0.0.x`-until-first-alpha ladder), and the troubleshooting
|
|
137
|
+
bullet becomes an honest pin-your-version pre-alpha caveat rather than a stale non-goal.
|
|
138
|
+
|
|
139
|
+
- **`private: true` removed: `@cosyte/dicom` can publish.** The flag dated to the very first
|
|
10
140
|
scaffold commit and was never explained; `changeset publish` silently skips a private package, so
|
|
11
141
|
this repo was the one parser that could not reach npm even once its pipeline worked. It also
|
|
12
142
|
contradicted the `publishConfig: { access: "public" }` in the same file. Removed as part of the
|
|
@@ -18,25 +148,25 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
18
148
|
reference, retargeted at `src/version.ts`) and the `version` script that runs it after
|
|
19
149
|
`changeset version`, so the bump and the `VERSION` constant land in the same commit.
|
|
20
150
|
- **`VERSION` is no longer typed as a string literal.** It was declared `export const VERSION =
|
|
21
|
-
"0.0.0"`, giving it the literal type `"0.0.0"
|
|
151
|
+
"0.0.0"`, giving it the literal type `"0.0.0"`, so the exported type would change on every
|
|
22
152
|
release, making each version bump a breaking type change. Now annotated `: string`, matching the
|
|
23
153
|
`hl7` reference. Type-only; the runtime value is unchanged. Done now because the package is
|
|
24
|
-
unpublished
|
|
154
|
+
unpublished. After the first publish this would itself be a breaking change.
|
|
25
155
|
|
|
26
156
|
- **The Release workflow can actually start.** `.github/workflows/release.yml` calls the shared
|
|
27
157
|
`cosyte/.github` pipeline, which requests `contents`/`id-token`/`pull-requests: write`, but declared
|
|
28
|
-
no `permissions:` of its own
|
|
158
|
+
no `permissions:` of its own, so it inherited the repo default of `contents: read`. A called
|
|
29
159
|
workflow may only downgrade the caller's `GITHUB_TOKEN`, never escalate it, so GitHub rejected the
|
|
30
160
|
workflow at startup (~1s, no jobs, no logs). Every Release run from June 2026 until now failed this
|
|
31
161
|
way, unnoticed, because a `startup_failure` produces no logs to read. The caller job now declares
|
|
32
|
-
the three scopes explicitly. CI-only
|
|
162
|
+
the three scopes explicitly. CI-only: no runtime or API change.
|
|
33
163
|
|
|
34
164
|
### Security
|
|
35
165
|
|
|
36
|
-
- **Dev-dependency advisory remediation (no runtime impact
|
|
166
|
+
- **Dev-dependency advisory remediation (no runtime impact: the published
|
|
37
167
|
artifact is unchanged).** Added scoped `pnpm.overrides` pinning two
|
|
38
168
|
transitive **dev/build-time** packages to their patched releases: `esbuild`
|
|
39
|
-
(`>=0.27.3 <0.28.1` → `0.28.1`; GHSA dev-server path-traversal
|
|
169
|
+
(`>=0.27.3 <0.28.1` → `0.28.1`; GHSA dev-server path-traversal, not
|
|
40
170
|
reachable here: the library builds via `tsup`/`vitest` and never runs
|
|
41
171
|
`esbuild serve`) and the `@changesets/parse` copy of `js-yaml`
|
|
42
172
|
(`>=4.0.0 <4.2.0` → `4.2.0`; GHSA-h67p-54hq-rp68 merge-key DoS). The
|
|
@@ -49,19 +179,25 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
49
179
|
|
|
50
180
|
### Added
|
|
51
181
|
|
|
182
|
+
- **Trademark notice (`TRADEMARKS.md`).** This package names third-party systems to describe what it
|
|
183
|
+
interoperates with; the notice records that cosyte is not affiliated with, endorsed by, or
|
|
184
|
+
sponsored by any of them, that every reference is descriptive, and that the built-in profiles are
|
|
185
|
+
authored from public sources only. Added to `files` so it ships inside the published tarball, not
|
|
186
|
+
just on GitHub. Documentation only: no runtime or API change.
|
|
187
|
+
|
|
52
188
|
- **Lossless File Meta round-trip.** The parser now retains non-modeled
|
|
53
|
-
`(0002,xxxx)` File Meta elements
|
|
189
|
+
`(0002,xxxx)` File Meta elements (e.g. `(0002,0017)`/`(0002,0018)`
|
|
54
190
|
Sending/Receiving AE Title, `(0002,0100)` Private Information Creator UID,
|
|
55
|
-
`(0002,0102)` Private Information
|
|
191
|
+
`(0002,0102)` Private Information) as raw on-wire bytes on the new
|
|
56
192
|
`FileMeta.extraElements` view (each a `FileMetaRawElement` carrying the tag,
|
|
57
193
|
its Explicit-VR-LE VR, and a defensively copied even-length value). The
|
|
58
194
|
serializer merges these with the typed fields and emits the whole group in
|
|
59
195
|
ascending tag order (PS3.5 §7.4) with a recomputed `(0002,0000)` group length
|
|
60
|
-
(PS3.10 §7.1), so an exotic File Meta group now round-trips byte-for-byte
|
|
196
|
+
(PS3.10 §7.1), so an exotic File Meta group now round-trips byte-for-byte,
|
|
61
197
|
not just the typed fields. New exported type: `FileMetaRawElement`. This
|
|
62
198
|
resolves the Phase 5 serializer known-limitation ("only the typed `FileMeta`
|
|
63
199
|
fields round-trip").
|
|
64
|
-
- **Documentation completeness (Phase 8).** Rewrote `README.md` into a full developer guide
|
|
200
|
+
- **Documentation completeness (Phase 8).** Rewrote `README.md` into a full developer guide: quickstart,
|
|
65
201
|
feature tour, a "DICOM in 90 seconds" primer, the two access patterns, an 80/20 **cookbook** (index a
|
|
66
202
|
folder, build routing keys, read pixel-interpretation metadata safely, de-identify, bridge to FHIR
|
|
67
203
|
`ImagingStudy` / HL7 v2, round-trip serialize), the four-tier tolerance model, the warning/fatal code
|
|
@@ -69,13 +205,13 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
69
205
|
export now carries a JSDoc `@example`. Extended the dual ESM/CJS smoke harnesses to exercise the full
|
|
70
206
|
Phase 1–7 published surface so the documented entrypoints are guaranteed importable from both module
|
|
71
207
|
systems. Also corrected an `intro.md` snippet that referenced a nonexistent `ds.pixelData` getter (the
|
|
72
|
-
real accessor is `ds.get("PixelData")?.value`). Docs-only
|
|
208
|
+
real accessor is `ds.get("PixelData")?.value`). Docs-only: no runtime API change.
|
|
73
209
|
- **Metadata-level de-identification (Phase 7).** New `deidentify(ds, options?)` applies the PS3.15
|
|
74
210
|
Annex E **Basic Application Level Confidentiality Profile** plus the nine metadata-affecting Annex E
|
|
75
211
|
Options (`RetainUIDs`, `RetainLongitudinalTemporal`, `RetainPatientCharacteristics`,
|
|
76
212
|
`RetainDeviceIdentity`, `RetainInstitutionIdentity`, `RetainSafePrivate`, `CleanDescriptors`,
|
|
77
213
|
`CleanStructuredContent`, `CleanGraphics`), driven by the generated Table E.1-1 action map. It is a
|
|
78
|
-
**pure** function
|
|
214
|
+
**pure** function: the input `Dataset` is never mutated; it returns a fresh de-identified `Dataset`
|
|
79
215
|
and a value-free `DeidentifyReport` (tags, keywords, resolved action codes, the UID map, warnings).
|
|
80
216
|
Each attribute's action (`D` dummy, `Z` zero-length, `X` remove, `K` keep, `C` clean, `U` consistent
|
|
81
217
|
UID) is resolved from the Basic Profile, overridden by any active Option; conditional codes (`Z/D`,
|
|
@@ -101,31 +237,31 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
101
237
|
`escalate` (Tier-2 warning codes promoted to a thrown `DicomParseError`), `suppress` (codes silenced
|
|
102
238
|
as a documented benign quirk of the source), and `privateTags` (a private-creator-keyed overlay that
|
|
103
239
|
resolves the Implicit VR of vendor private data elements). Private resolution is keyed on the file's
|
|
104
|
-
**live** private-creator string and the canonical `"GGGGxxLL"` key (PS3.5 §7.8.1)
|
|
105
|
-
hard-coded block number
|
|
240
|
+
**live** private-creator string and the canonical `"GGGGxxLL"` key (PS3.5 §7.8.1), never a
|
|
241
|
+
hard-coded block number, so the same vendor schema resolves regardless of which block it landed in.
|
|
106
242
|
Profiles compose via `extends` (de-duplicated lineage, union of escalations/suppressions, child-wins
|
|
107
243
|
dictionary merge) and expose a deterministic `describe()` summary. Five built-ins ship under the
|
|
108
244
|
frozen `profiles` namespace: three vendor overlays (`ge`, `siemens`, `philips`, grounded in the
|
|
109
245
|
public GDCM / dcm4che / dcm2niix private dictionaries) and two posture presets (`strict` escalates
|
|
110
246
|
integrity-relevant warnings; `lenient` suppresses cosmetic, high-volume ones). A creator the active
|
|
111
247
|
profile does not recognize degrades to generic `UN` plus the new `DICOM_PRIVATE_CREATOR_UNKNOWN`
|
|
112
|
-
warning
|
|
248
|
+
warning, never a wrong decode. Selecting a profile never changes a correct decode. New public
|
|
113
249
|
exports: `defineProfile`, `profiles`, `ProfileDefinitionError`, and the types `Profile`,
|
|
114
250
|
`PrivateTagDefinition`, `DefineProfileOptions`, `ProfilePrivateTags`; `ParseOptions` gains an
|
|
115
251
|
optional `profile` field. The reserved `DICOM_PRIVATE_CREATOR_UNKNOWN` code is now actively emitted
|
|
116
252
|
(no change to the `WARNING_CODES` registry surface).
|
|
117
253
|
- **Spec-clean Part 10 serializer (Phase 5).** New `serializeDicom(ds)` writes a `Dataset` back to a
|
|
118
|
-
DICOM Part 10 `Buffer
|
|
254
|
+
DICOM Part 10 `Buffer`, the conservative half of Postel's Law. Emits the 128-byte zero preamble +
|
|
119
255
|
`DICM`, a File Meta group (always Explicit VR LE) with a computed `(0002,0000)` group length and
|
|
120
256
|
conservative Type-1 defaults (File Meta Version `0x0001`, cosyte Implementation Class UID under the
|
|
121
|
-
`2.25` UUID arc), then the dataset body in the dataset's own transfer syntax
|
|
257
|
+
`2.25` UUID arc), then the dataset body in the dataset's own transfer syntax (**no transcode**)
|
|
122
258
|
across all four v1 syntaxes (Implicit VR LE, Explicit VR LE/BE, Deflated Explicit VR LE). Scalar
|
|
123
259
|
values are padded to even length per PS3.5 §6.2 (`0x00` for `UI`/byte-stream VRs, `0x20` for text),
|
|
124
260
|
short vs long-form headers are chosen by VR per §7.1.2 (`SV`/`UV` long-form), retired `(gggg,0000)`
|
|
125
261
|
group-length elements are omitted per §7.2, and sequence + encapsulated-pixel-data spans pass
|
|
126
|
-
through byte-for-byte per §7.5 / §A.4. Pure function
|
|
262
|
+
through byte-for-byte per §7.5 / §A.4. Pure function: the input `Dataset` is never mutated.
|
|
127
263
|
- **Serializer error taxonomy.** New `DicomSerializeError` with codes `MISSING_TRANSFER_SYNTAX`
|
|
128
|
-
(no File Meta Transfer Syntax UID) and `UNSUPPORTED_TRANSFER_SYNTAX` (a UID outside the v1 set)
|
|
264
|
+
(no File Meta Transfer Syntax UID) and `UNSUPPORTED_TRANSFER_SYNTAX` (a UID outside the v1 set),
|
|
129
265
|
separate from the parser's fatal codes and the value layer's `DicomValueError`. The message is
|
|
130
266
|
built only from the code + the offending Transfer Syntax UID (structural facts), never a decoded
|
|
131
267
|
value, so it is always safe to log. New public exports: `serializeDicom`, `DicomSerializeError`,
|
|
@@ -137,7 +273,7 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
137
273
|
fields round-trip; any other `(0002,xxxx)` element a source file carried (e.g. `(0002,0100)` Private
|
|
138
274
|
Information Creator UID) is dropped at _parse_ time (the Phase 2 `FileMeta` view does not model it)
|
|
139
275
|
and so cannot be re-emitted. The preamble is normalized to zeros and odd-length values are padded
|
|
140
|
-
even
|
|
276
|
+
even. The output stays spec-clean but is not a byte-identical copy of a non-conformant input.
|
|
141
277
|
|
|
142
278
|
### Tests
|
|
143
279
|
|
|
@@ -158,7 +294,7 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
158
294
|
tuple plus Other Patient IDs (so a caller never matches on a bare, non-unique `(0010,0020)`) and
|
|
159
295
|
keeps `PN` structured; `study`/`series` surface the cross-system UIDs, accession number, modality
|
|
160
296
|
and Frame of Reference UID. `image` surfaces the pixel-interpretation + geometry metadata a
|
|
161
|
-
renderer needs
|
|
297
|
+
renderer needs, with the safety-critical omissions intact: `rescaleSlope` is **absent** (not `1`)
|
|
162
298
|
when the tag is absent, `signed` is absent (never guessed) unless `(0028,0103)` was present,
|
|
163
299
|
`photometricInterpretation` is never defaulted to `MONOCHROME2`, and the three pixel-spacing tags
|
|
164
300
|
(`(0028,0030)` / `(0018,1164)` / `(0018,2010)`) are distinct, never aliased.
|
|
@@ -166,7 +302,7 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
166
302
|
Per-Frame-else-Shared (PS3.3 §C.7.6.16): Pixel Measures, Plane Position, Plane Orientation, Pixel
|
|
167
303
|
Value Transformation, Frame VOI LUT. `image.isEnhancedMultiFrame` flags such objects.
|
|
168
304
|
- **Value-layer error taxonomy.** New `DicomValueError` (codes `FRAME_INDEX_OUT_OF_RANGE`,
|
|
169
|
-
`MISSING_REQUIRED_FUNCTIONAL_GROUP`)
|
|
305
|
+
`MISSING_REQUIRED_FUNCTIONAL_GROUP`), separate from the parser's four fatal codes. The helpers are
|
|
170
306
|
otherwise fail-safe (typed-absent for missing data) and throw only for a structural contract
|
|
171
307
|
violation; the error message carries only structural facts (indices, tag/macro names), never a
|
|
172
308
|
decoded PHI value.
|
|
@@ -179,7 +315,7 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
179
315
|
`CodedConcept`, `RealWorldValueMap`, `FrameFunctionalGroups`, `ValueErrorCode`, plus
|
|
180
316
|
`VALUE_ERROR_CODES` and the `readCode` / `codingSchemeOid` / `CODING_SCHEME_OIDS` helpers.
|
|
181
317
|
- **VR value decode + dataset navigation (Phase 3).** `Element.value` now lazily decodes (and
|
|
182
|
-
memoizes) an element's raw bytes into a typed, discriminated `DicomValue` covering all 34 VRs
|
|
318
|
+
memoizes) an element's raw bytes into a typed, discriminated `DicomValue` covering all 34 VRs:
|
|
183
319
|
numbers (`US/UL/SS/SL/FL/FD`), 64-bit `bigint`s (`SV/UV`), attribute tags (`AT`), person names
|
|
184
320
|
(`PN` → 3-group / 5-component), strings, free text, numeric strings (`DS/IS` → `number | null`,
|
|
185
321
|
never `NaN`→0), temporal values (`DA/TM/DT`), sequences (`SQ` → threaded items), and raw `binary`
|
|
@@ -203,7 +339,7 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
203
339
|
(`_arbitraries.ts`) plus invariant suites for round-trip fidelity, lenient-mode robustness,
|
|
204
340
|
parsed-model immutability, warning/fatal-code stability (snapshot), and a byte-parser fuzz sweep
|
|
205
341
|
that feeds arbitrary buffers + random truncations and asserts the parser only ever throws a
|
|
206
|
-
sanctioned Tier-3 `DicomParseError
|
|
342
|
+
sanctioned Tier-3 `DicomParseError`, never an unexpected error, hang, or OOM. No public API
|
|
207
343
|
change. (devDeps: `@cosyte/test-utils@^0.0.1`, `fast-check@3.23.2`.)
|
|
208
344
|
|
|
209
345
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# @cosyte/dicom
|
|
2
2
|
|
|
3
|
-
> Read a real-world, vendor-quirky DICOM Part 10 file and pull the metadata you need in one line
|
|
3
|
+
> Read a real-world, vendor-quirky DICOM Part 10 file and pull the metadata you need in one line, without having read the DICOM standard.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@cosyte/dicom)
|
|
6
6
|
[](https://github.com/cosyte/dicom/actions/workflows/ci.yml)
|
|
7
7
|
[](./LICENSE)
|
|
8
8
|
[](https://nodejs.org)
|
|
9
9
|
|
|
10
|
-
A developer-focused DICOM Part 10 parser and utility library for Node.js and TypeScript. **Metadata-first**: it reads the headers
|
|
10
|
+
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.
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -16,7 +16,7 @@ A developer-focused DICOM Part 10 parser and utility library for Node.js and Typ
|
|
|
16
16
|
Useful output after install + parse. No DICOM spec knowledge required.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
# pnpm (recommended)
|
|
19
|
+
# pnpm (recommended). Also works with: npm install @cosyte/dicom | yarn add @cosyte/dicom
|
|
20
20
|
pnpm add @cosyte/dicom
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -26,25 +26,25 @@ import { parseDicom } from "@cosyte/dicom";
|
|
|
26
26
|
|
|
27
27
|
const ds = parseDicom(await readFile("study.dcm"));
|
|
28
28
|
|
|
29
|
-
ds.get("PatientName")?.value; // PN value
|
|
30
|
-
ds.study.instanceUid; // "1.2.840.…"
|
|
31
|
-
ds.image.rescaleSlope; // number | undefined
|
|
29
|
+
ds.get("PatientName")?.value; // PN value: structured, never flattened
|
|
30
|
+
ds.study.instanceUid; // "1.2.840.…": the global study anchor
|
|
31
|
+
ds.image.rescaleSlope; // number | undefined: undefined means "absent", never 1
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
That's the pitch: no config, no schema upload, no spec lookup. The parser accepts vendor-quirky input by default
|
|
34
|
+
That's the pitch: no config, no schema upload, no spec lookup. The parser accepts vendor-quirky input by default (it tolerates a missing preamble, a wrong File Meta group length, odd-length values, and the dozen-or-so deviations real scanners emit) and records each as a stable warning code rather than failing. You reach for typed value decode, safety-critical views, profiles, or the serializer when you want them.
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
38
|
## Features
|
|
39
39
|
|
|
40
|
-
- **One-line metadata extraction
|
|
41
|
-
- **Two access patterns
|
|
42
|
-
- **Lazy typed value decode
|
|
43
|
-
- **Real-world tolerance, Postel's Law
|
|
44
|
-
- **Source/vendor profile system
|
|
45
|
-
- **Metadata-level de-identification
|
|
46
|
-
- **Spec-clean serializer
|
|
47
|
-
- **Strict TypeScript, dual ESM + CJS, Node ≥ 22
|
|
40
|
+
- **One-line metadata extraction**: `ds.patient`, `ds.study`, `ds.series`, `ds.image`: typed, fail-safe views over the safety-critical attributes. No `(group,element)` tags to memorise.
|
|
41
|
+
- **Two access patterns**: named views, or structural `ds.get("PatientName")` / `ds.get("(0010,0010)")` by keyword or tag, plus `ds.elements()` to walk everything.
|
|
42
|
+
- **Lazy typed value decode**: `element.value` decodes raw bytes into a discriminated `DicomValue` across all 34 VRs (numbers, `bigint`s, person names, dates/times, sequences, raw `binary`), honoring `(0008,0005)` Specific Character Set through nested items.
|
|
43
|
+
- **Real-world tolerance, Postel's Law**: a lenient reader emits 24 stable warning codes for what it tolerated; only 4 truly-structural conditions are fatal. The serializer always writes spec-clean Part 10.
|
|
44
|
+
- **Source/vendor profile system**: `defineProfile()` + 5 built-ins (`ge`, `siemens`, `philips`, `strict`, `lenient`) that only ever _tighten or annotate_ a parse, resolving vendor private tags by the file's live Private Creator string, never a wrong decode.
|
|
45
|
+
- **Metadata-level de-identification**: `deidentify()` applies the PS3.15 Annex E Basic Profile + the nine metadata Options, returning a fresh dataset and a value-free audit report.
|
|
46
|
+
- **Spec-clean serializer**: `serializeDicom(ds)` round-trips a dataset back to Part 10 bytes in its source transfer syntax (no transcode), with correct File Meta group length, even-length padding, byte-exact sequence passthrough, and lossless File Meta: non-modeled `(0002,xxxx)` elements are preserved and re-emitted in tag order.
|
|
47
|
+
- **Strict TypeScript, dual ESM + CJS, Node ≥ 22**: `noUncheckedIndexedAccess`, no `any`, JSDoc + `@example` on every public export feeding your editor's IntelliSense. Zero runtime dependencies today.
|
|
48
48
|
|
|
49
49
|
---
|
|
50
50
|
|
|
@@ -52,18 +52,18 @@ That's the pitch: no config, no schema upload, no spec lookup. The parser accept
|
|
|
52
52
|
|
|
53
53
|
A DICOM Part 10 file is a 128-byte preamble + the `DICM` magic, then a **File Meta** group (always Explicit VR Little Endian) naming the **transfer syntax**, then the **dataset**: a flat, tag-ordered stream of **data elements**.
|
|
54
54
|
|
|
55
|
-
Each element is identified by a `(group,element)` **tag** (e.g. `(0010,0010)` = Patient's Name) and carries a two-letter **VR** (Value Representation
|
|
55
|
+
Each element is identified by a `(group,element)` **tag** (e.g. `(0010,0010)` = Patient's Name) and carries a two-letter **VR** (Value Representation: `PN`, `DA`, `US`, `SQ`, …) that says how to decode its bytes. Some elements are **sequences** (`SQ`): ordered lists of **items**, each a nested dataset. The transfer syntax decides endianness, whether VRs are written explicitly, and whether the stream is deflated.
|
|
56
56
|
|
|
57
57
|
```
|
|
58
58
|
DICOM file
|
|
59
59
|
├── preamble (128 bytes) + "DICM"
|
|
60
|
-
├── File Meta group (0002,xxxx)
|
|
60
|
+
├── File Meta group (0002,xxxx) : transfer syntax UID, SOP Class/Instance UID
|
|
61
61
|
└── dataset
|
|
62
62
|
├── (0008,0060) Modality "CT"
|
|
63
63
|
├── (0010,0010) PatientName PN "Doe^Jane"
|
|
64
64
|
├── (0020,000D) StudyInstanceUID UI "1.2.840.…"
|
|
65
65
|
├── (0028,0100) BitsAllocated US 16
|
|
66
|
-
└── (7FE0,0010) PixelData OW «raw bytes
|
|
66
|
+
└── (7FE0,0010) PixelData OW «raw bytes, not decoded»
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
`@cosyte/dicom` reads all of that leniently and hands you typed accessors over it. The one thing it deliberately does **not** do is decode the pixels.
|
|
@@ -74,11 +74,11 @@ DICOM file
|
|
|
74
74
|
|
|
75
75
|
### Safety-critical views
|
|
76
76
|
|
|
77
|
-
The four views
|
|
77
|
+
The four views (`patient`, `study`, `series`, `image`) pull the right field out of the right tag for the jobs that matter most, and they are **fail-safe**: a missing value is typed-absent (`undefined`), never a substituted default.
|
|
78
78
|
|
|
79
79
|
```ts
|
|
80
80
|
const p = ds.patient;
|
|
81
|
-
p.id; // "MRN-42"
|
|
81
|
+
p.id; // "MRN-42": NOT globally unique on its own…
|
|
82
82
|
p.issuerOfId; // …pair with the issuer for cross-system matching
|
|
83
83
|
p.name?.alphabetic.familyName; // structured PN, never flattened
|
|
84
84
|
|
|
@@ -95,7 +95,7 @@ s.accessionNumber; // ties the study to the HIS order (0008,0050)
|
|
|
95
95
|
ds.get("Modality"); // by keyword
|
|
96
96
|
ds.get("(0008,0060)"); // same element, by tag
|
|
97
97
|
ds.has("PixelData"); // boolean
|
|
98
|
-
ds.elements(); // readonly Element[]
|
|
98
|
+
ds.elements(); // readonly Element[]: walk everything
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
### Typed values
|
|
@@ -120,7 +120,7 @@ Recipes for the jobs a metadata parser is actually asked to do. Every attribute
|
|
|
120
120
|
|
|
121
121
|
### Index a folder of studies
|
|
122
122
|
|
|
123
|
-
Pull a few fields out of each file to build a searchable index
|
|
123
|
+
Pull a few fields out of each file to build a searchable index: the bread-and-butter PACS/archive job.
|
|
124
124
|
|
|
125
125
|
```ts
|
|
126
126
|
import { readFile } from "node:fs/promises";
|
|
@@ -145,18 +145,18 @@ Nothing here throws on a quirky file; absent fields come back `undefined`. Check
|
|
|
145
145
|
|
|
146
146
|
### Build routing keys
|
|
147
147
|
|
|
148
|
-
Routing and reconciliation hang off a small set of identifiers. Surface them correctly
|
|
148
|
+
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 C.7.1.1).
|
|
149
149
|
|
|
150
150
|
```ts
|
|
151
151
|
// Hierarchy keys for filing into Study → Series → Instance:
|
|
152
|
-
const studyKey = ds.study.instanceUid; // (0020,000D)
|
|
152
|
+
const studyKey = ds.study.instanceUid; // (0020,000D): global anchor
|
|
153
153
|
const seriesKey = ds.series.instanceUid; // (0020,000E)
|
|
154
154
|
const instanceKey = ds.image.sopInstanceUid; // (0008,0018)
|
|
155
155
|
|
|
156
|
-
// Cross-system patient key
|
|
156
|
+
// Cross-system patient key: id ALONE is not unique; pair it with the issuer:
|
|
157
157
|
const p = ds.patient;
|
|
158
158
|
const patientKey = `${p.issuerOfId ?? "?"}|${p.id ?? "?"}`;
|
|
159
|
-
p.otherIds; // (0010,1002) Other Patient IDs Sequence
|
|
159
|
+
p.otherIds; // (0010,1002) Other Patient IDs Sequence: additional {id, issuer} pairs
|
|
160
160
|
```
|
|
161
161
|
|
|
162
162
|
### Read pixel-interpretation metadata safely
|
|
@@ -170,12 +170,12 @@ img.rescaleIntercept; // (0028,1052) apply as: stored*slope + intercept
|
|
|
170
170
|
img.signed; // true/false only if (0028,0103) Pixel Representation was present; else undefined
|
|
171
171
|
img.bitsStored; // (0028,0101)
|
|
172
172
|
img.photometricInterpretation; // (0028,0004) never defaulted to MONOCHROME2
|
|
173
|
-
img.pixelSpacing; // (0028,0030) patient-plane mm
|
|
173
|
+
img.pixelSpacing; // (0028,0030) patient-plane mm, distinct from imagerPixelSpacing
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
-
> **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
|
|
176
|
+
> **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("(2005,1409)")` (optionally under `profiles.philips`).
|
|
177
177
|
|
|
178
|
-
For Enhanced multi-frame objects, `image.frame(i)` resolves each frame's functional-group macros Per-Frame-else-Shared (PS3.3 C.7.6.16). It throws a `DicomValueError`
|
|
178
|
+
For Enhanced multi-frame objects, `image.frame(i)` resolves each frame's functional-group macros Per-Frame-else-Shared (PS3.3 C.7.6.16). 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.
|
|
179
179
|
|
|
180
180
|
```ts
|
|
181
181
|
if (img.isEnhancedMultiFrame) {
|
|
@@ -187,15 +187,15 @@ if (img.isEnhancedMultiFrame) {
|
|
|
187
187
|
|
|
188
188
|
### De-identify before sharing
|
|
189
189
|
|
|
190
|
-
`deidentify()` applies the PS3.15 Annex E Basic Application Level Confidentiality Profile
|
|
190
|
+
`deidentify()` applies the PS3.15 Annex E Basic Application Level Confidentiality Profile (replacing, emptying, or removing every attribute the standard lists as identifying) and returns a fresh dataset plus a value-free report.
|
|
191
191
|
|
|
192
192
|
```ts
|
|
193
193
|
import { parseDicom, deidentify, serializeDicom } from "@cosyte/dicom";
|
|
194
194
|
|
|
195
195
|
const { dataset, report } = deidentify(parseDicom(buf));
|
|
196
|
-
const safe = serializeDicom(dataset); // safe to share
|
|
196
|
+
const safe = serializeDicom(dataset); // safe to share: input dataset never mutated
|
|
197
197
|
|
|
198
|
-
report.attributes.length; // count of attributes acted on (each carries tag/keyword/action
|
|
198
|
+
report.attributes.length; // count of attributes acted on (each carries tag/keyword/action, no values)
|
|
199
199
|
report.warnings; // e.g. DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED
|
|
200
200
|
```
|
|
201
201
|
|
|
@@ -230,7 +230,7 @@ const imagingStudy = {
|
|
|
230
230
|
const obr18 = ds.study.accessionNumber;
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
-
> Series and SOP Instance UIDs are **not** represented in HL7 v2
|
|
233
|
+
> Series and SOP Instance UIDs are **not** represented in HL7 v2. Image-level identity lives only in DICOM.
|
|
234
234
|
|
|
235
235
|
### Round-trip: read, edit, re-serialize
|
|
236
236
|
|
|
@@ -238,10 +238,10 @@ const obr18 = ds.study.accessionNumber;
|
|
|
238
238
|
import { parseDicom, serializeDicom } from "@cosyte/dicom";
|
|
239
239
|
|
|
240
240
|
const ds = parseDicom(buf);
|
|
241
|
-
const out = serializeDicom(ds); // spec-clean Part 10, same transfer syntax
|
|
241
|
+
const out = serializeDicom(ds); // spec-clean Part 10, same transfer syntax, no transcode
|
|
242
242
|
```
|
|
243
243
|
|
|
244
|
-
The serializer is the conservative half of Postel's Law: it rebuilds the File Meta group with a correct `(0002,0000)` length, pads values to even length, and re-emits sequences and encapsulated pixel data byte-for-byte. The File Meta group round-trips losslessly
|
|
244
|
+
The serializer is the conservative half of Postel's Law: it rebuilds the File Meta group with a correct `(0002,0000)` length, pads values to even length, and re-emits sequences and encapsulated pixel data byte-for-byte. The File Meta group round-trips losslessly: non-modeled `(0002,xxxx)` elements (Sending/Receiving AE Title, Private Information, etc.) are preserved on parse and re-emitted in ascending tag order.
|
|
245
245
|
|
|
246
246
|
---
|
|
247
247
|
|
|
@@ -256,13 +256,13 @@ import { parseDicom, profiles } from "@cosyte/dicom";
|
|
|
256
256
|
const ds = parseDicom(buf, { profile: profiles.siemens });
|
|
257
257
|
```
|
|
258
258
|
|
|
259
|
-
A profile bundles three things that only ever **tighten or annotate** a parse
|
|
259
|
+
A profile bundles three things that only ever **tighten or annotate** a parse, never loosen it past the lenient default:
|
|
260
260
|
|
|
261
|
-
- **Private-dictionary overlay
|
|
262
|
-
- **Escalations
|
|
263
|
-
- **Suppressions
|
|
261
|
+
- **Private-dictionary overlay**: resolves the Implicit VR of vendor private data elements by the file's _live_ Private Creator string (e.g. `"SIEMENS CSA HEADER"`), keyed canonically as `"GGGGxxLL"` (PS3.5 §7.8.1), never a hard-coded block number. (This is why Agfa IMPAX re-assigning blocks still resolves.) An unknown creator degrades to `UN` plus a `DICOM_PRIVATE_CREATOR_UNKNOWN` warning.
|
|
262
|
+
- **Escalations**: Tier-2 warning codes promoted to a thrown `DicomParseError` (a stricter posture for known-unsafe deviations).
|
|
263
|
+
- **Suppressions**: benign, high-volume warning codes silenced for a known-quirky source.
|
|
264
264
|
|
|
265
|
-
Five built-ins ship under the `profiles` namespace: `ge`, `siemens`, `philips` (vendor overlays, grounded in the public GDCM / dcm4che / dcm2niix dictionaries) and `strict` / `lenient` (posture presets). Build your own with `defineProfile()
|
|
265
|
+
Five built-ins ship under the `profiles` namespace: `ge`, `siemens`, `philips` (vendor overlays, grounded in the public GDCM / dcm4che / dcm2niix dictionaries) and `strict` / `lenient` (posture presets). Build your own with `defineProfile()`. It validates input, composes via `extends`, and returns a frozen profile:
|
|
266
266
|
|
|
267
267
|
```ts
|
|
268
268
|
import { defineProfile, profiles } from "@cosyte/dicom";
|
|
@@ -284,7 +284,7 @@ At an RSNA-era interoperability test, ~80% of real-world patient CDs failed stri
|
|
|
284
284
|
|
|
285
285
|
| Tier | Behavior | When | Example codes |
|
|
286
286
|
| ---- | -------------- | ------------------------------ | ------------------------ |
|
|
287
|
-
| 0 | Silent | Spec-compliant input |
|
|
287
|
+
| 0 | Silent | Spec-compliant input | none |
|
|
288
288
|
| 1 | Auto-handled | Trivial deviation, no warning | trailing-space tidy |
|
|
289
289
|
| 2 | Warning | Recoverable deviation | `DICOM_MISSING_PREAMBLE` |
|
|
290
290
|
| 3 | Fatal (always) | Unrecoverable structural error | `NOT_DICOM_PART_10` |
|
|
@@ -304,7 +304,7 @@ for (const w of ds.warnings) {
|
|
|
304
304
|
|
|
305
305
|
The 24 Tier-2 codes (`DICOM_MISSING_PREAMBLE`, `DICOM_FILE_META_GROUP_LENGTH_MISMATCH`, `DICOM_UN_PARSED_AS_SQ`, `DICOM_ODD_LENGTH_VALUE_PADDED`, `DICOM_PRIVATE_CREATOR_UNKNOWN`, `DICOM_VR_MISMATCH`, `DICOM_DA_LEGACY_FORMAT`, … ) live in [`src/parser/warnings.ts`](./src/parser/warnings.ts). Narrow on `w.code === WARNING_CODES.…` for typo-free comparisons, or pass `{ onWarning }` to `parseDicom` to stream them.
|
|
306
306
|
|
|
307
|
-
The 4 Tier-3 fatal codes
|
|
307
|
+
The 4 Tier-3 fatal codes (`NOT_DICOM_PART_10`, `INVALID_FILE_META`, `UNSUPPORTED_TRANSFER_SYNTAX`, `EMPTY_INPUT`) always throw a `DicomParseError`; they represent input the parser cannot meaningfully recover.
|
|
308
308
|
|
|
309
309
|
---
|
|
310
310
|
|
|
@@ -330,7 +330,7 @@ try {
|
|
|
330
330
|
|
|
331
331
|
### `DicomValueError`
|
|
332
332
|
|
|
333
|
-
Thrown only by `image.frame(i)
|
|
333
|
+
Thrown only by `image.frame(i)`: `FRAME_INDEX_OUT_OF_RANGE` for an index outside `[0, numberOfFrames)`, or `MISSING_REQUIRED_FUNCTIONAL_GROUP` when an enhanced object lacks a required geometry macro in both the Per-Frame and Shared groups. Value decode (`element.value`) never throws. It warns and returns `null`/typed-absent instead.
|
|
334
334
|
|
|
335
335
|
### `DicomSerializeError`
|
|
336
336
|
|
|
@@ -338,7 +338,7 @@ Thrown by `serializeDicom` for `MISSING_TRANSFER_SYNTAX` (the dataset names no t
|
|
|
338
338
|
|
|
339
339
|
### `ProfileDefinitionError` · `DeidentifyError`
|
|
340
340
|
|
|
341
|
-
`defineProfile()` throws `ProfileDefinitionError` for a structurally invalid profile; `deidentify()` throws `DeidentifyError` (`INVALID_OPTIONS`) for an unknown Retain option or malformed UID root. Both messages carry only structural facts
|
|
341
|
+
`defineProfile()` throws `ProfileDefinitionError` for a structurally invalid profile; `deidentify()` throws `DeidentifyError` (`INVALID_OPTIONS`) for an unknown Retain option or malformed UID root. Both messages carry only structural facts (option names, the UID root), never a decoded value.
|
|
342
342
|
|
|
343
343
|
---
|
|
344
344
|
|
|
@@ -360,18 +360,23 @@ Supported transfer syntaxes (structure for all; **pixels never decoded**): Impli
|
|
|
360
360
|
|
|
361
361
|
v1 is metadata-feature-complete. Future companion packages (separate repos, demand-sequenced):
|
|
362
362
|
|
|
363
|
-
- **`@cosyte/dicom-pixel
|
|
364
|
-
- **`@cosyte/dicom-net
|
|
365
|
-
- **`@cosyte/dicomweb
|
|
363
|
+
- **`@cosyte/dicom-pixel`**: pixel decode/decompression, frame extraction, burned-in-annotation cleaning.
|
|
364
|
+
- **`@cosyte/dicom-net`**: DIMSE network services.
|
|
365
|
+
- **`@cosyte/dicomweb`**: QIDO / WADO / STOW REST clients.
|
|
366
366
|
|
|
367
367
|
---
|
|
368
368
|
|
|
369
369
|
## Contributing
|
|
370
370
|
|
|
371
|
-
Vendor-quirk fixtures (synthetic or properly de-identified), profile improvements, and dictionary corrections are all welcome
|
|
371
|
+
Vendor-quirk fixtures (synthetic or properly de-identified), profile improvements, and dictionary corrections are all welcome. The more real-world edge cases the test suite covers, the more robust the parser gets. See [CONTRIBUTING.md](./CONTRIBUTING.md) if present, or open an issue.
|
|
372
372
|
|
|
373
373
|
---
|
|
374
374
|
|
|
375
|
+
## Trademarks
|
|
376
|
+
|
|
377
|
+
GE, Siemens, and Philips are trademarks of their respective owners. cosyte is not affiliated with, endorsed by, or
|
|
378
|
+
sponsored by any of them. The names identify the vendors whose private dictionaries the built-in profiles resolve. See [TRADEMARKS.md](./TRADEMARKS.md).
|
|
379
|
+
|
|
375
380
|
## License
|
|
376
381
|
|
|
377
382
|
[MIT](./LICENSE) © Cosyte
|
package/TRADEMARKS.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Trademarks
|
|
2
|
+
|
|
3
|
+
`@cosyte/dicom` is an independent open-source project. cosyte is **not affiliated with, endorsed by,
|
|
4
|
+
or sponsored by** any company named in this repository or its documentation.
|
|
5
|
+
|
|
6
|
+
## Why these names appear
|
|
7
|
+
|
|
8
|
+
DICOM private data elements carry no on-wire VR under Implicit VR Little Endian, so resolving them
|
|
9
|
+
requires knowing whose private blocks they belong to. Naming the vendor is the only way to
|
|
10
|
+
describe (or correctly decode) its private dictionary.
|
|
11
|
+
|
|
12
|
+
Every reference is **descriptive**: it identifies whose private dictionary a profile resolves, and nothing more. Naming a system is the only way to say
|
|
13
|
+
whether a library works with it.
|
|
14
|
+
|
|
15
|
+
## Where the profiles come from
|
|
16
|
+
|
|
17
|
+
The built-in source profiles are authored through this package's own public `defineProfile()` API,
|
|
18
|
+
from public open-source dictionaries (the GDCM private dictionary and dcm2niix tag notes) and
|
|
19
|
+
from vendor-published schema identifiers. Each profile documents its sources inline. They embed
|
|
20
|
+
no privileged, confidential, or reverse-engineered material, and no patient data.
|
|
21
|
+
|
|
22
|
+
## Names referenced
|
|
23
|
+
|
|
24
|
+
| Name | Where it appears |
|
|
25
|
+
| ------- | ----------------------------------------------------------------------------- |
|
|
26
|
+
| GE | `profiles.ge`: a built-in profile name, resolving the `GEMS_*` private blocks |
|
|
27
|
+
| Siemens | `profiles.siemens`: a built-in profile name |
|
|
28
|
+
| Philips | `profiles.philips`: a built-in profile name |
|
|
29
|
+
|
|
30
|
+
All product names, logos, and brands are the property of their respective owners. Use of a name here
|
|
31
|
+
does not imply any affiliation with, or endorsement by, its owner. If you own one of these marks and
|
|
32
|
+
would like a reference changed, please open an issue.
|