@cosyte/dicom 0.0.12 → 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 +631 -3
- package/README.md +14 -10
- package/dist/index.cjs +656 -291
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +319 -88
- package/dist/index.d.ts +319 -88
- package/dist/index.mjs +657 -292
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
<
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
<a href="https://cosyte.com">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cosyte.com/tile/cosyte-lockup-tile-on-dark-1200x300.png">
|
|
4
|
+
<img alt="Cosyte: a plus mark set in two overlapping rounded squares, one solid and one outlined, beside the Cosyte wordmark" src="https://cosyte.com/tile/cosyte-lockup-tile-on-light-1200x300.png">
|
|
5
|
+
</picture>
|
|
6
|
+
</a>
|
|
5
7
|
|
|
6
8
|
# @cosyte/dicom
|
|
7
9
|
|
|
@@ -14,6 +16,8 @@
|
|
|
14
16
|
|
|
15
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.
|
|
16
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
|
+
|
|
17
21
|
---
|
|
18
22
|
|
|
19
23
|
## Quickstart
|
|
@@ -155,7 +159,7 @@ A quirky object is tolerated rather than rejected, and absent fields come back `
|
|
|
155
159
|
|
|
156
160
|
### Build routing keys
|
|
157
161
|
|
|
158
|
-
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.
|
|
159
163
|
|
|
160
164
|
```ts
|
|
161
165
|
// Hierarchy keys for filing into Study → Series → Instance:
|
|
@@ -185,7 +189,7 @@ img.pixelSpacing; // (0028,0030) patient-plane mm, distinct from imagerPixelSpac
|
|
|
185
189
|
|
|
186
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`).
|
|
187
191
|
|
|
188
|
-
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.
|
|
189
193
|
|
|
190
194
|
```ts
|
|
191
195
|
if (img.isEnhancedMultiFrame) {
|
|
@@ -305,7 +309,7 @@ At an RSNA-era interoperability test, ~80% of real-world patient CDs failed stri
|
|
|
305
309
|
| 2 | Warning | Recoverable deviation | `DICOM_MISSING_PREAMBLE` |
|
|
306
310
|
| 3 | Fatal (always) | Unrecoverable structural error | `NOT_DICOM_PART_10` |
|
|
307
311
|
|
|
308
|
-
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.
|
|
309
313
|
|
|
310
314
|
```ts
|
|
311
315
|
import { parseDicom, WARNING_CODES } from "@cosyte/dicom";
|
|
@@ -334,7 +338,7 @@ The library throws five typed errors, all exported from the package barrel. Warn
|
|
|
334
338
|
|
|
335
339
|
### `DicomParseError`
|
|
336
340
|
|
|
337
|
-
Thrown by `parseDicom` on one of the 4 Tier-3 fatal codes. Carries the byte position, a registry-composed message, and a 16-byte hex `snippet` of the source. The snippet is raw input: treat it as PHI and redact it at your own boundary.
|
|
341
|
+
Thrown by `parseDicom` on one of the 4 Tier-3 fatal codes. Carries the byte position, a registry-composed message, and a 16-byte hex `snippet` of the source. **The message is looked up in a frozen registry and its factories take no tag and no wire-length parameter**, so neither can be interpolated: a fatal about a length field that lies fires precisely when bytes inside somebody's value are being read as a header, which is what used to make those two fields document content. `err.byteOffset` locates the element instead. The snippet is raw input and is cut in the frame that offset is counted in: treat it as PHI and redact it at your own boundary.
|
|
338
342
|
|
|
339
343
|
```ts
|
|
340
344
|
import { parseDicom, DicomParseError, FATAL_CODES } from "@cosyte/dicom";
|
|
@@ -368,9 +372,9 @@ Thrown by `serializeDicom` for `MISSING_TRANSFER_SYNTAX` (the dataset names no t
|
|
|
368
372
|
|
|
369
373
|
- **Pixel data.** No decode/decompression, no rendering, no measurements: Pixel Data is exposed as raw bytes. And v1 does not read a **compressed object at all**, not even structurally: a transfer syntax outside the four listed below is the fatal `UNSUPPORTED_TRANSFER_SYNTAX`, so JPEG / JPEG-LS / JPEG2000 / RLE / HTJ2K objects do not parse. → `@cosyte/dicom-pixel`.
|
|
370
374
|
- **Burned-in PHI.** v1 **warns** it cannot remove burned-in annotation; a "de-identified" output is **metadata-de-identified only**.
|
|
371
|
-
- **A sequence the parser could not open: content is dropped, not passed through.** `deidentify()` recurses only into a sequence whose items the parser materialized.
|
|
375
|
+
- **A sequence the parser could not open: content is dropped, not passed through.** `deidentify()` recurses only into a sequence whose items the parser materialized. An `SQ` element with no items is now **emptied**: its bytes are Data Sets by PS3.5 §7.5.1 and PS3.15 §E.1.1 obliges the de-identifier to reach the listed attributes inside them, so a run that cannot enumerate them must not ship them. `report.unauditableSequences` names the carrier and the byte length dropped (capped at 64 entries; the emptying itself is never capped), and `DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE` is raised. Expect **data loss** on such a file. The parse refusal is usually a sender-side encoding defect, but not always: a **conformant** file nested deeper than this library's own `NESTING_DEPTH_LIMIT` of 64 is refused the same way and loses that sequence too. **A private `SQ` kept under `RetainSafePrivate` + a `Profile` was exempt through `0.0.10` and is not any more** (`DICOM-PRIVATE-SQ-CARVE-OUT`): the profile vouches for the private attribute, which is what PS3.15 §E.3.10 licenses, not for an item stream nothing could read. **That closure keys on the parse tree, and the profile is a second authority** (`DICOM-PRIVATE-SQ-PARSE-VR`, closed after `0.0.10`): a private carrier the profile declares `SQ` is emptied and named in `report.unauditableSequences` even when the parse tree calls it something else, keeping the VR the file carried. The two disagree whenever the profile did not reach `parseDicom` (Implicit VR LE writes no VR for a private tag, so the element arrives `UN`) or the sender wrote a binary VR under Explicit VR, where the wire wins - the latter on a fully conformant file, with `ds.warnings` **empty**, so the report is the only channel. **Pass your profile to `parseDicom` as well as to `deidentify()`** and the sequence is walked and its non-PHI content kept rather than dropped. **One shape is still exempt and still leaks:** an undefined-length **`UN`** the CP-246 descent could not read as a sequence, **where no profile named the attribute**. It keeps `vr === "UN"`, and the rule cannot be extended to `UN` in general because every ordinary `UN` element also has no items. Where a profile _does_ declare that private attribute `SQ`, the rule above reaches it and empties it, because that test does not look at the length field. **And the limit on that closure is not what an earlier wording here claimed - the two sets are incomparable, not nested - so that wording is deleted rather than adjusted again:** the rule empties what a `Profile` declares `SQ`, and **every other declaration falls through and is kept verbatim** whenever the embedded-attribute scanner cannot read the value. That scanner reads **string carriers only** and decodes candidate headers in the **file's own encoding**, so a carrier the sender wrote `OB`, `OW` or `UN` is never scanned whatever the profile calls it, and neither is a string carrier holding Explicit-VR-shaped bytes inside an Implicit VR LE file. A well-formed item stream carrying a `(0010,0010)` Patient's Name survives into de-identified output in all of those cases, silently, with an empty `report.unauditableSequences`. The surface is pinned as a measured matrix in `test/integration/deident-private-reservation.test.ts` rather than listed in prose, because the list has been wrong twice. Closing any of it needs a content test on exactly the VRs arbitrary bytes are for, which would also empty conformant binary values, so it is an open product question and not a defect with a known fix. For all of these, the reliable test remains `el.items === undefined` on the element itself.
|
|
372
376
|
- **An unrecognized Explicit VR is read (and written) long-form, and `deidentify()` still empties the element.** PS3.5 2026c §6.2: "All new VRs defined in future versions of DICOM **shall** be of the same Data Element Structure as defined in [§7.1.2] with reserved bytes after the VR and a 32-bit unsigned integer VL." So two on-wire VR bytes outside the 34 this release knows get a **12-byte header**, exactly as `OB` and `UT` do, on both the read and the write path. Before `0.0.9` the reader took the 8-byte form, which read the length out of the two bytes §6.2 reserves. What such a file did then was **shape-specific** - sometimes a whole-object refusal, sometimes a clean parse into a tree the sender did not write - which is why this package ships `scripts/measure-unrecognized-vr.ts` and no one-sentence account of it. The value is otherwise handled like any other: a declared length past the end of the buffer, or an undefined length, is refused exactly as it is for every other non-`SQ` VR. **The trade is stated rather than buried**: a sender that ignores §6.2 and writes an unrecognized VR short-form produced a readable object before and is refused now. `deidentify()` **still empties** such an element, because reading a header is not the same as knowing what its value means - Table E.1-1 acts per attribute, and nothing can say whether a VR from a later edition holds a name, a date or an opaque blob. `report.undefinedVrElements` names the **byte offset** and the byte length dropped (capped at 64 entries; the emptying itself is never capped) and `DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE` is raised. It names **no tag**, deliberately and uniquely among the report's findings: the header may still have been fabricated out of the middle of some element's value, in which case its tag bytes are document content - and nothing distinguishes that case from an honestly-written unrecognized VR, so the tag is withheld either way. An Implicit VR LE file cannot reach any of this: there the VR comes from the dictionary. `UN` is one of the 34 and is not affected. **There is no exemption** - unlike the sequence rule above, `RetainSafePrivate` does not keep such an element.
|
|
373
|
-
- **`RetainSafePrivate` retains nothing inside ANY Item of a sequence whose item stream over-runs its own declared length.** An Item that declares more bytes than its enclosing Sequence does absorbs whatever follows the sequence, so a private data element the sender wrote at the **root** - with no Private Creator reserving its block there - can land beside a creator that is genuine Item content and be kept verbatim. Measured on the published `0.0.8` tarball it was, with `report.removedPrivateTags` reading `[]`, the object stamped `(0012,0062) Patient Identity Removed = YES` and `ds.warnings` empty: a stamp that outran the redaction. (**There is no `0.0.9` on the registry** - `package.json` carried it and the publish never happened, an npm `E404` - so earlier wording here that said "through `0.0.9`" named a version that does not exist. Measured on the current published `0.0.10`, this direction is fixed.) PS3.5 2026c §7.8.1 scopes a private block reservation to the Item ("The scope of the reservation is just within the Item"), so a file that contradicts itself about where the Item ends does not determine which reservation covers an element; PS3.15 2026c §E.3.10 retains only what is **known** safe and requires that "all other Private Attributes shall be removed **or processed in the element-specific manner recommended by Deidentification Action (0008,0307), if present within Private Data Element Characteristics Sequence (0008,0300)**" (a two-branch clause; this library does not implement `(0008,0307)`, so removal is the branch available to it). So every private element the recursion **reaches** inside such a sequence, at every depth below it, is removed and named in `report.removedPrivateTags`. **The unit is the SEQUENCE, not the individual Item**: `descendSequence` decides once and applies it to every item, so a two-item sequence where only item 1 over-runs also loses a genuine block sitting in the honest item 0 (measured; the grid sweeps single-item fixtures, so that extra cost is NOT inside the priced 20). **
|
|
377
|
+
- **`RetainSafePrivate` retains nothing inside ANY Item of a sequence whose item stream over-runs its own declared length.** An Item that declares more bytes than its enclosing Sequence does absorbs whatever follows the sequence, so a private data element the sender wrote at the **root** - with no Private Creator reserving its block there - can land beside a creator that is genuine Item content and be kept verbatim. Measured on the published `0.0.8` tarball it was, with `report.removedPrivateTags` reading `[]`, the object stamped `(0012,0062) Patient Identity Removed = YES` and `ds.warnings` empty: a stamp that outran the redaction. (**There is no `0.0.9` on the registry** - `package.json` carried it and the publish never happened, an npm `E404` - so earlier wording here that said "through `0.0.9`" named a version that does not exist. Measured on the current published `0.0.10`, this direction is fixed.) PS3.5 2026c §7.8.1 scopes a private block reservation to the Item ("The scope of the reservation is just within the Item"), so a file that contradicts itself about where the Item ends does not determine which reservation covers an element; PS3.15 2026c §E.3.10 retains only what is **known** safe and requires that "all other Private Attributes shall be removed **or processed in the element-specific manner recommended by Deidentification Action (0008,0307), if present within Private Data Element Characteristics Sequence (0008,0300)**" (a two-branch clause; this library does not implement `(0008,0307)`, so removal is the branch available to it). So every private element the recursion **reaches** inside such a sequence, at every depth below it, is removed and named in `report.removedPrivateTags`. **The unit is the SEQUENCE, not the individual Item**: `descendSequence` decides once and applies it to every item, so a two-item sequence where only item 1 over-runs also loses a genuine block sitting in the honest item 0 (measured; the grid sweeps single-item fixtures, so that extra cost is NOT inside the priced 20). **The one route this rule did not reach is closed** (`DICOM-PRIVATE-SQ-CARVE-OUT`): a **private `SQ`** the profile vouches for was kept verbatim before the descent ran, so nothing inside it was examined; it is now walked like any other sequence, and a private element the file's own length fields pulled into its items is refused there. **The direction is not fail-safe on its own and the opposite claim is retracted**: it depends on where the sender put the Private Creator. Both **absorb** placements are pinned by tests, and so is the **eject** placement (below). Treat `DICOM_ITEM_CROSSES_SEQUENCE_END` as "this file's structure is in dispute", never as a statement about what was retained. **The price is measured, not asserted**: on `scripts/measure-sq-bound-grid.ts`, 58 cells lose a private value and **20 of them were not leaking anything** (the creator and the data element were both genuine Item content). Retention on a file that does not contradict itself is unchanged, and **no reading changes** - no parser file is touched. This is not a parser bound: an over-declaring Item and an under-declaring Sequence are byte-identical, so nothing on the wire can separate them.
|
|
374
378
|
- **`RetainSafePrivate` also retains nothing a Data Set holds AFTER a sequence whose own contents contradict the extent it declared.** The mirror direction: an Item that _under_-declares **ejects** its trailing elements out into the enclosing Data Set, and a Private Creator that lands there reserves a block for elements the sender never put beside it - the same false attestation. **Measured on the published `0.0.10` tarball, which is the current registry version**: `report.removedPrivateTags` `[]`, the value in the serialized output, `(0012,0062) = YES`, `ds.warnings` empty and no throw under `{ strict: true }`. It applies **in every Data Set, not only the root**: an inner sequence ejecting a creator into the still-usable Item that encloses it is measured and pinned. The cut is **positional**, so a reservation the sender wrote **ahead of** the offending sequence is untouched, and the same reservation written **after** it is refused. **Two predicates, because the parser records the same contradiction two ways**: under Explicit VR the item stream over-runs and the span shows up as `rawBytes.length > length`; under Implicit VR LE that path slices the item stream, so nothing over-runs at all and the descent is refused instead (`DICOM_SQ_NOT_DESCENDED`, `Element.items === undefined`). The second predicate is **broader than the ejection it is here for, deliberately**: it says the parser could not walk the sequence, which other unwalkable item streams also reach. **And two BOUNDS, not one, because a Data Set is a `Map<Tag, Element>`**: an ejected element whose tag the Data Set already holds overwrites in place and inherits the earlier element's position, ahead of the sequence it came out of, so `Element.byteOffset` is checked beside the index. **That collision also destroys the root's own value at parse time** - `PRE-EXISTING`, unchanged here, and pinned by a test so it is not mistaken for something this rule handles. It was silent when that was written; `DICOM_DUPLICATE_TAG_IN_DATA_SET` reports it now (below), and the destruction itself is still unchanged. **The price is measured**: on `scripts/measure-sq-bound-grid.ts` all **22** cells that retained a private value at the root on a self-contradicting file whose honest control removes it go to **0**, at the cost of **56** further root retentions on self-contradicting files whose honest control does keep the value (the column is `de-identified OUTPUT lost a marker (cost)`, reading 78; the parse-tree `LOST`/`GAINED a marker value` counters beside it read 0). Retention on a file that does not contradict itself is unchanged (9 at the root, 6 in an Item), and **no reading changes**: 0 cells differ in any parse respect, 0 cells whose reading differs, and no parser file is touched.
|
|
375
379
|
- **An over-declared Value Length into a binary carrier still leaks, and it is a known trade.** The mirror case - an _over_-declared length that swallows the following element into an `OB`/`OW`/`US`/`UN` value - is **not** detected, and a `(0010,0020)` inside it reaches de-identified output with no warning and no report entry. Arbitrary bytes are exactly what those VRs are for, so no content test can decide it; the only candidate remedy was measured and empties conformant binary values (a legal LUT or blob deleted because 8 of its bytes read as a zero-length `(0010,0020)`). String carriers **are** covered, because there the same bytes are provably outside the VR's repertoire. If you accept files from a sender you do not control, treat a binary attribute's length as untrusted.
|
|
376
380
|
- **`(0012,0063)` De-identification Method is ADDED TO, so a prior de-identifier's text is carried into the output.** PS3.15 2026c §E.1.1 says a text string describing the method "shall be inserted in or added to" that attribute, and this library used to **replace** it - destroying the provenance chain the attribute exists to carry, silently, on a file whose earlier pass may be the one a recipient was relying on. It now appends its own text as a further value of the `1-n` attribute, after a `\`, copying the prior bytes through verbatim. **The method string is itself a `1-n` value**: it is split on `\` and only the values not already recorded are added. **Trailing SPACE and NUL are padding rather than content** (PS3.5 2026c Table 6.2-1's `LO` row, which describes a **Value**, and `LO` is `1-n`), so they are ignored **per value, on both sides** when your string is matched against what is already recorded, and trimmed from the value written. Repeated de-identification is therefore a fixed point **from the first pass**, for every string: with or without a delimiter, and with a pad byte on any value, last or not. Leading spaces are yours and are written through untouched; a method string that is padding only records nothing. **One bound, and it is not cosmetic**: `LO` is a short-form VR whose Value Length field is 16 bits, so when the join would not fit, the prior value is **replaced** rather than added to and `report.warnings` carries `DICOM_DEIDENT_METHOD_NOT_ADDED`. Read that code as "the length ceiling was reached", never as "every fallback is disclosed": a `(0012,0063)` a file encoded under a VR other than `LO` is also replaced, and that one has its own code, `DICOM_DEIDENT_METHOD_NOT_LO`. An element the serializer cannot encode would take the whole de-identified object down, and truncating the sender's earlier records instead would be a policy the standard does not state. A `deidentificationMethod` longer than that ceiling on its own is unchanged from every earlier release and still fails to serialize. **The cost is stated rather than glossed**: `(0012,0063)` is **not in Table E.1-1**, so the Basic Profile keeps it exactly as it keeps every other unlisted attribute, and a sender who wrote something identifying into it now sees that text in de-identified output where the replacement used to remove it as a side effect. That is the retained-by-omission posture of every unlisted attribute, not a rule this insertion states; it is pinned by a residual test that asserts the cost. **It is not silent**: `report.warnings` carries `DICOM_DEIDENT_METHOD_PRIOR_RETAINED` whenever prior bytes from the source file survive into `(0012,0063)`, because an object stamped `(0012,0062) = YES` carrying text nothing in the run inspected is a stamp that outran the redaction. It is **not** on `report.retained` - that field lists the Annex E option sets active for the run, and a kept attribute is not one of them. `(0012,0062)` Patient Identity Removed is still **replaced** with `YES` - the same paragraph of §E.1.1 uses the other verb for it ("shall be replaced or added to"), and the asymmetry is the standard's. A `(0012,0063)` a file encoded under a VR other than `LO` is replaced rather than appended to, and `report.warnings` now carries `DICOM_DEIDENT_METHOD_NOT_LO` when it is: the join concatenates `LO` values with the `5CH` delimiter, which is not defined over arbitrary octets, so replacing is deliberate and only the silence was the defect. **And the method text this library writes for itself is now multi-valued, one Value for the Profile and one per active Option.** PS3.5 2026c Table 6.2-1 caps an `LO` at **64 characters per Value** and `(0012,0063)` is `1-n`; the single-value string it wrote before measured **76** characters with no options, **130** with three and **272** with all nine, so every object de-identified without a caller-supplied method, in every release up to and including this one, carried a value no `LO` may legally hold - in the one attribute a strict receiver reads to decide whether the object was de-identified at all. **Re-de-identifying such an object keeps that 76-character Value**, because a prior record is added to rather than rewritten (measured: a flat 138 bytes over four passes, values of 76 and 61, `DICOM_DEIDENT_METHOD_PRIOR_RETAINED` raised for the retention and nothing said about the length). Your own `deidentificationMethod` is **not** bounded for you either: split it on `\` yourself if a strict receiver is in your path.
|