@cosyte/dicom 0.0.1 → 0.0.4

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/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 — without having read the DICOM standard.
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
  [![npm version](https://img.shields.io/npm/v/@cosyte/dicom.svg)](https://www.npmjs.com/package/@cosyte/dicom)
6
6
  [![CI](https://img.shields.io/github/actions/workflow/status/cosyte/dicom/ci.yml?branch=main&label=CI)](https://github.com/cosyte/dicom/actions/workflows/ci.yml)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
8
8
  [![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](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 — 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.
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) — also works with: npm install @cosyte/dicom | yarn add @cosyte/dicom
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 — 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
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 — 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.
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** — `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.
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 — `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.
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) — transfer syntax UID, SOP Class/Instance UID
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 — not decoded»
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 — `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.
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" — NOT globally unique on its own…
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[] — walk everything
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 — the bread-and-butter PACS/archive job.
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 — a Patient ID without its issuer is ambiguous across systems (PS3.3 C.7.1.1).
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) — global anchor
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 — id ALONE is not unique; pair it with the issuer:
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 — additional {id, issuer} pairs
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 — distinct from imagerPixelSpacing
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 — reach them with `ds.get("(2005,1409)")` (optionally under `profiles.philips`).
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` — carrying only structural facts, never PHI — for an out-of-range frame or a required geometry macro missing from both groups.
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 — replacing, emptying, or removing every attribute the standard lists as identifying — and returns a fresh dataset plus a value-free report.
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 — input dataset never mutated
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 — no values)
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
 
@@ -210,6 +210,10 @@ deidentify(parseDicom(buf), {
210
210
 
211
211
  This is **metadata-level** de-identification. Pixel cleaning is out of scope: when a file carries burned-in annotation this layer cannot remove, you get a `DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED` warning rather than a false sense of safety (pixel cleaning is deferred to `@cosyte/dicom-pixel`).
212
212
 
213
+ The action table comes from NEMA's PS3.15 2026c DocBook, the normative publication of the standard, rather than from a third-party mirror of it, so the current edition's patient attributes are removed rather than quietly kept. That includes the three rows the standard states as a repeating-group mask rather than a single tag: `(50xx,xxxx)` Curve Data, `(60xx,3000)` Overlay Data and `(60xx,4000)` Overlay Comments are matched in every overlay or curve group the standard defines, removed, and named in the report with the mask that matched them. Overlay comments in particular are a common carrier for text typed onto a study, so a clean report on a file that still held them was worse than no report.
214
+
215
+ The groups a mask covers are the sixteen even ones PS3.5 bounds it to (`6000`-`601E`, `5000`-`501E`), not any four hex digits. Reading `xx` as a wildcard would strip attributes the standard never marked, which is data loss on a call you asked to be conservative. That bound is read out of PS3.5 itself, pinned by SHA-256 the same way the action table is, rather than copied into the source by hand: the current edition states the overlay range, and the curve range comes from the 2004 edition its own note delegates to, with the two required to agree where they overlap.
216
+
213
217
  ### Bridge to FHIR / HL7 v2
214
218
 
215
219
  A common consulting ask is joining imaging to the rest of the record. The authoritative crosswalk is the FHIR [`ImagingStudy` "Mappings for DICOM"](https://build.fhir.org/imagingstudy-mappings.html) tab. The join keys a metadata parser must surface correctly:
@@ -230,7 +234,7 @@ const imagingStudy = {
230
234
  const obr18 = ds.study.accessionNumber;
231
235
  ```
232
236
 
233
- > Series and SOP Instance UIDs are **not** represented in HL7 v2 — image-level identity lives only in DICOM.
237
+ > Series and SOP Instance UIDs are **not** represented in HL7 v2. Image-level identity lives only in DICOM.
234
238
 
235
239
  ### Round-trip: read, edit, re-serialize
236
240
 
@@ -238,10 +242,10 @@ const obr18 = ds.study.accessionNumber;
238
242
  import { parseDicom, serializeDicom } from "@cosyte/dicom";
239
243
 
240
244
  const ds = parseDicom(buf);
241
- const out = serializeDicom(ds); // spec-clean Part 10, same transfer syntax — no transcode
245
+ const out = serializeDicom(ds); // spec-clean Part 10, same transfer syntax, no transcode
242
246
  ```
243
247
 
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.
248
+ 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
249
 
246
250
  ---
247
251
 
@@ -256,13 +260,13 @@ import { parseDicom, profiles } from "@cosyte/dicom";
256
260
  const ds = parseDicom(buf, { profile: profiles.siemens });
257
261
  ```
258
262
 
259
- A profile bundles three things that only ever **tighten or annotate** a parse — never loosen it past the lenient default:
263
+ A profile bundles three things that only ever **tighten or annotate** a parse, never loosen it past the lenient default:
260
264
 
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.
265
+ - **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.
266
+ - **Escalations**: Tier-2 warning codes promoted to a thrown `DicomParseError` (a stricter posture for known-unsafe deviations).
267
+ - **Suppressions**: benign, high-volume warning codes silenced for a known-quirky source.
264
268
 
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:
269
+ 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
270
 
267
271
  ```ts
268
272
  import { defineProfile, profiles } from "@cosyte/dicom";
@@ -284,7 +288,7 @@ At an RSNA-era interoperability test, ~80% of real-world patient CDs failed stri
284
288
 
285
289
  | Tier | Behavior | When | Example codes |
286
290
  | ---- | -------------- | ------------------------------ | ------------------------ |
287
- | 0 | Silent | Spec-compliant input | — |
291
+ | 0 | Silent | Spec-compliant input | none |
288
292
  | 1 | Auto-handled | Trivial deviation, no warning | trailing-space tidy |
289
293
  | 2 | Warning | Recoverable deviation | `DICOM_MISSING_PREAMBLE` |
290
294
  | 3 | Fatal (always) | Unrecoverable structural error | `NOT_DICOM_PART_10` |
@@ -304,7 +308,7 @@ for (const w of ds.warnings) {
304
308
 
305
309
  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
310
 
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.
311
+ 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
312
 
309
313
  ---
310
314
 
@@ -330,7 +334,7 @@ try {
330
334
 
331
335
  ### `DicomValueError`
332
336
 
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.
337
+ 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
338
 
335
339
  ### `DicomSerializeError`
336
340
 
@@ -338,7 +342,7 @@ Thrown by `serializeDicom` for `MISSING_TRANSFER_SYNTAX` (the dataset names no t
338
342
 
339
343
  ### `ProfileDefinitionError` · `DeidentifyError`
340
344
 
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.
345
+ `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
346
 
343
347
  ---
344
348
 
@@ -360,18 +364,23 @@ Supported transfer syntaxes (structure for all; **pixels never decoded**): Impli
360
364
 
361
365
  v1 is metadata-feature-complete. Future companion packages (separate repos, demand-sequenced):
362
366
 
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.
367
+ - **`@cosyte/dicom-pixel`**: pixel decode/decompression, frame extraction, burned-in-annotation cleaning.
368
+ - **`@cosyte/dicom-net`**: DIMSE network services.
369
+ - **`@cosyte/dicomweb`**: QIDO / WADO / STOW REST clients.
366
370
 
367
371
  ---
368
372
 
369
373
  ## Contributing
370
374
 
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.
375
+ 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
376
 
373
377
  ---
374
378
 
379
+ ## Trademarks
380
+
381
+ GE, Siemens, and Philips are trademarks of their respective owners. cosyte is not affiliated with, endorsed by, or
382
+ sponsored by any of them. The names identify the vendors whose private dictionaries the built-in profiles resolve. See [TRADEMARKS.md](./TRADEMARKS.md).
383
+
375
384
  ## License
376
385
 
377
386
  [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.