@cosyte/dicom
Namespaces
Classes
Dataset
One parsed DICOM dataset - the structural shell.
Public data: fileMeta, warnings. The internal element map is stored
on _elements (protected) and reached through the public
get / has / elements / getAll navigation API.
Example
import { parseDicom } from "@cosyte/dicom";
const ds = parseDicom(buf);
console.log(ds.fileMeta?.transferSyntaxUID);
for (const w of ds.warnings) {
console.warn(w.code, w.message);
}
Extended by
Constructors
Constructor
new Dataset(
init):Dataset
Internal
Construct a new structural Dataset. The warnings array is frozen at
the model boundary.
Parameters
init
DatasetInit
Returns
Properties
_elements
protectedreadonly_elements:ReadonlyMap<string,Element>
Internal
Element map, keyed by uppercase 8-hex tag. Reached publicly through the
navigation surface; kept protected here so only subclasses (Item, and
any later extension) can introspect it directly.
fileMeta
readonlyfileMeta:FileMeta|undefined
warnings
readonlywarnings: readonlyDicomParseWarning[]
Accessors
directory
Get Signature
get directory():
DicomDirectory|undefined
The DICOMDIR Directory Record tree, or undefined when this Dataset is not
a DICOMDIR (its File Meta Media Storage SOP Class UID is not
1.2.840.10008.1.3.10). Read from the four offset attributes (PS3.3 2026d
Table F.3-3): root is the record (0004,1200) names and each
(0004,1400) successor, and each record's lowerLevel is the record its
(0004,1420) names and each successor. An offset resolves only when it is
exactly the file offset of an Item of the Directory Record Sequence
(0004,1220); one that is not names no record, and parseDicom warns
(DICOM_DIRECTORY_OFFSET_UNRESOLVED, DICOM_DIRECTORY_OFFSET_MALFORMED,
DICOM_DIRECTORY_RECORD_REVISITED, DICOM_DIRECTORY_OFFSET_DEFLATED).
Memoised on first read.
Limits: record keys are not checked against PS3.3 F.5, the File-set
Consistency Flag and (0004,1202)'s agreement with the end of the root
chain are not checked, and under Deflated Explicit VR LE no offset resolves,
so every record is listed in records and none is linked.
Example
import { parseDicom } from "@cosyte/dicom";
const dir = parseDicom(buf).directory;
for (const patient of dir?.root ?? []) {
for (const study of patient.lowerLevel) {
for (const series of study.lowerLevel) {
for (const image of series.lowerLevel) console.log(image.referencedFileId);
}
}
}
Returns
DicomDirectory | undefined
image
Get Signature
get image():
ImageView
Pixel-interpretation + geometry view. Surfaces exactly
what a renderer needs without guessing: rescaleSlope/signed/
photometricInterpretation stay absent rather than defaulted, and the
three pixel-spacing tags are distinct. Memoised on first read.
Example
import { parseDicom } from "@cosyte/dicom";
const img = parseDicom(buf).image;
img.rescaleSlope; // undefined ⇒ MUST NOT assume 1
Returns
patient
Get Signature
get patient():
PatientView
Patient-identity view. Fail-safe typed-absent fields; id is
not globally unique - match on the {id, issuerOfId, ...} tuple
plus otherIds, never on a bare (0010,0020). Memoised on first read.
Example
import { parseDicom } from "@cosyte/dicom";
const p = parseDicom(buf).patient;
p.name?.alphabetic.familyName; // structured PN, never flattened
Returns
series
Get Signature
get series():
SeriesView
Series-identity & co-registration view. A shared
frameOfReferenceUid means images are spatially co-registered.
Memoised on first read.
Example
import { parseDicom } from "@cosyte/dicom";
const s = parseDicom(buf).series;
s.modality; // "CT"
Returns
study
Get Signature
get study():
StudyView
Study-identity view. instanceUid is the cross-system study
key; accessionNumber ties it to the order. Memoised on first read.
Example
import { parseDicom } from "@cosyte/dicom";
const s = parseDicom(buf).study;
s.instanceUid; // "1.2.840.113619..."
Returns
Methods
elements()
elements(): readonly
Element[]
All elements in this dataset, in parse (insertion) order.
Returns
readonly Element[]
Example
import { parseDicom } from "@cosyte/dicom";
const ds = parseDicom(buf);
for (const el of ds.elements()) console.log(el.tag, el.vr);
get()
get(
tag):Element|undefined
Look up a single element by tag. Tags are normalised to 8-char
uppercase hex, so "7fe00010" and "7FE00010" resolve to the same
element. Returns undefined when the tag is absent.
Parameters
tag
string
Returns
Element | undefined
Example
import { parseDicom } from "@cosyte/dicom";
const ds = parseDicom(buf);
const rows = ds.get("00280010"); // Rows
if (rows?.value.kind === "numbers") console.log(rows.value.values[0]);
getAll()
getAll(
tag): readonlyElement[]
All elements matching a tag as an array (never undefined). A dataset
holds at most one element per tag, so this returns a 0- or 1-length
array - the convenience complement of Dataset.get for callers
that prefer an always-array shape.
Parameters
tag
string
Returns
readonly Element[]
Example
import { parseDicom } from "@cosyte/dicom";
const ds = parseDicom(buf);
for (const el of ds.getAll("00080060")) console.log(el.value);
has()
has(
tag):boolean
true when an element with the given tag is present (case-insensitive).
Parameters
tag
string
Returns
boolean
Example
import { parseDicom } from "@cosyte/dicom";
const ds = parseDicom(buf);
if (ds.has("00100010")) console.log("has Patient's Name");
DeidentifyError
Thrown by deidentify when it will not de-identify, for one of two
reasons a caller tells apart by code (see DEIDENTIFY_ERROR_CODES):
an author-time misconfiguration of the call (INVALID_OPTIONS: an unknown
Retain option, a malformed UID root), or a Dataset under a Transfer Syntax this
de-identifier refuses (UNSUPPORTED_TRANSFER_SYNTAX: the four JPIP Referenced
syntaxes). Distinct from the parser's fatal codes, the value layer's
DicomValueError, and the serializer's DicomSerializeError. The message
carries only structural facts (option names, the UID root) or a fixed string -
never a decoded value, and for UNSUPPORTED_TRANSFER_SYNTAX nothing read from
the Dataset at all.
Example
import { deidentify, DeidentifyError } from "@cosyte/dicom";
try {
// @ts-expect-error - not a valid option
deidentify(ds, { retain: ["RetainEverything"] });
} catch (e) {
if (e instanceof DeidentifyError) console.error(e.code); // "INVALID_OPTIONS"
}
Extends
Error
Constructors
Constructor
new DeidentifyError(
message,code):DeidentifyError
Parameters
message
string
Human-readable, PHI-free description.
code
One of DEIDENTIFY_ERROR_CODES.
Returns
Overrides
Error.constructor
Properties
code
readonlycode:DeidentifyErrorCode
DicomParseError
Thrown by parseDicom when the input violates one of the four
unrecoverable Tier-3 structural rules - or, under { strict: true },
when any Tier-2 warning is escalated through the single emit
chokepoint. Carries byte-offset positional context plus a short
source snippet so consumers can log actionable errors.
Message format: [CODE] msg (offset=N frame=F), with … in a/b/c
appended when contextPath is provided.
Remarks
Snippets may contain PHI when parsing real clinical files - redact at the call site if required by your compliance posture. The library does not redact snippets itself.
byteOffset is only an index into your own buffer when offsetFrame is
"input". This parser reads a Data Set out of a slice in two situations -
a defined-length Sequence Item, and an SQ/UN descent - and out of an
inflated stream in a third, and an offset raised in any of them counts from
that buffer's byte 0. OFFSET_FRAMES names which; where a slice
begins is deliberately not published, because the distance between two
frames is a declared length off the wire.
snippet is cut in the frame offsetFrame names, on every fatal but one.
The exception is UNSUPPORTED_TRANSFER_SYNTAX, whose snippet slot carries
PS3.6's own NAME for the unsupported UID when the registry publishes one
("SMPTE ST 2110-20 Uncompressed Progressive Active Video"), and 16 raw
bytes only when it does not. That is
deliberate and predates the frame; it is named here because a universal about
snippet written without it is false on the code an object under a refused
Transfer Syntax reaches first. Everywhere else the two agree, so a consumer
that only wants the bytes at the offset already has them. The frame is what
a consumer needs before indexing anything of its OWN by byteOffset, which
is the case no field on this class used to cover.
Example
import { parseDicom, DicomParseError, OFFSET_FRAMES } from "@cosyte/dicom";
try {
parseDicom(buffer);
} catch (err) {
if (err instanceof DicomParseError && err.code === "NOT_DICOM_PART_10") {
// err.byteOffset, err.offsetFrame, err.snippet, err.contextPath
if (err.offsetFrame !== OFFSET_FRAMES.INPUT) {
// `byteOffset` counts from somewhere inside the file, not from its start.
}
}
}
Extends
Error
Constructors
Constructor
new DicomParseError(
code,message,byteOffset,offsetFrame,snippet,contextPath?):DicomParseError
Internal
Construct a new DicomParseError. All fields except contextPath are
required so every thrower populates positional context - and
offsetFrame is required for the same reason byteOffset is, because an
offset whose frame is optional is an offset whose frame is usually
missing.
Parameters
code
message
string
byteOffset
number
offsetFrame
snippet
string
contextPath?
readonly string[]
Returns
Overrides
Error.constructor
Properties
byteOffset
readonlybyteOffset:number
code
readonlycode:FatalCode
contextPath
readonlycontextPath: readonlystring[] |undefined
offsetFrame
readonlyoffsetFrame:OffsetFrame
The coordinate system DicomParseError.byteOffset is counted in. See OFFSET_FRAMES.
snippet
readonlysnippet:string
DicomSerializeError
Thrown by serializeDicom when a Dataset
cannot be emitted as spec-clean Part 10. Never carries a decoded value, so
it is safe to log without leaking PHI: the message is built only from the
code and structural constants, never from the Transfer Syntax UID, a length
or a byte the Dataset holds.
Example
import { parseDicom, serializeDicom, DicomSerializeError } from "@cosyte/dicom";
const ds = parseDicom(buf);
try {
serializeDicom(ds);
} catch (err) {
if (err instanceof DicomSerializeError && err.code === "MISSING_TRANSFER_SYNTAX") {
// dataset is missing the File Meta Transfer Syntax UID
}
if (err instanceof DicomSerializeError && err.code === "INVALID_ENCAPSULATED_PIXEL_DATA") {
// a section A.4 object whose top-level Pixel Data is not a fragment stream
}
if (err instanceof DicomSerializeError && err.code === "DIRECTORY_OFFSET_UNRESOLVED") {
// a DICOMDIR offset the writer cannot tie to a Directory Record
}
}
Extends
Error
Constructors
Constructor
new DicomSerializeError(
code,message):DicomSerializeError
Internal
Construct a new DicomSerializeError. The message MUST be built only
from structural facts (never a decoded attribute value) so the error is
always safe to log.
Parameters
code
message
string
Returns
Overrides
Error.constructor
Properties
code
readonlycode:SerializeErrorCode
DicomValueError
Thrown by the domain helpers for a structural contract violation that
cannot be answered safely (see module doc). Never carries a decoded
value, so it is safe to log without leaking PHI: the message is built
only from the code and structural facts (indices, tag/macro names).
Example
import { parseDicom, DicomValueError } from "@cosyte/dicom";
const img = parseDicom(buf).image;
try {
img.frame(9999);
} catch (err) {
if (err instanceof DicomValueError && err.code === "FRAME_INDEX_OUT_OF_RANGE") {
// handle the out-of-range request
}
}
Extends
Error
Constructors
Constructor
new DicomValueError(
code,message):DicomValueError
Internal
Construct a new DicomValueError. The message MUST be built only
from structural facts (never a decoded attribute value) so the error
is always safe to log.
Parameters
code
message
string
Returns
Overrides
Error.constructor
Properties
code
readonlycode:ValueErrorCode
Element
One DICOM Data Element as parsed.
Structural surface: tag, vr, vm, length, rawBytes,
byteOffset, privateCreator. On top of it sits a lazy, memoized
.value getter backed by the VR-aware decoders under src/dataset/vr/.
rawBytes retention behaviour: zero-copy Buffer.subarray view by
default (pins source ArrayBuffer); pass { copyValues: true } to
parseDicom for Buffer.from(slice) per element when the source
needs to be released.
Example
import { Buffer } from "node:buffer";
import { Element } from "@cosyte/dicom";
const el = new Element({
tag: "00100010",
vr: "PN",
vm: 1,
length: 8,
rawBytes: Buffer.from("DOE^JANE"),
byteOffset: 200,
littleEndian: true,
});
// el.tag === "00100010"; el.rawBytes is a Buffer of 8 bytes.
// el.value.kind === "personName".
Constructors
Constructor
new Element(
init):Element
Internal
Construct a new structural Element. The parser builds these directly
from on-wire bytes; consumers receive them via Dataset after
parseDicom.
Parameters
init
ElementInit
Returns
Properties
byteOffset
readonlybyteOffset:number
cp246Promoted
readonlycp246Promoted:boolean|undefined
Internal
Hint for the lazy SQ decoder. true when this Element was promoted
from VR=UN with undefined length to VR=SQ via the CP-246 fallback,
which is what selects the Implicit VR LE inner decoder. Always
undefined on standard SQ elements.
items
readonlyitems: readonlyItem[] |undefined
Parsed items for an SQ element; undefined otherwise.
length
readonlylength:number
littleEndian
readonlylittleEndian:boolean
Value byte order (per transfer syntax). See ElementInit.littleEndian.
privateCreator
readonlyprivateCreator:string|undefined
rawBytes
readonlyrawBytes:Buffer
specificCharacterSet
readonlyspecificCharacterSet: readonlystring[] |undefined
In-effect (0008,0005) terms, or undefined for the Default Repertoire.
tag
readonlytag:string
vm
readonlyvm:number
vr
readonlyvr:VR
Accessors
value
Get Signature
get value():
DicomValue
The decoded value of this element, by VR. Decode is lazy (the structural
parse stays eager; field decode runs on first access) and memoized - the
documented ~30× win on large studies where most fields are never read.
Fail-safe: never throws; a malformed value surfaces as typed-absent with
the deviation on the returned value's warnings.
Example
import { parseDicom } from "@cosyte/dicom";
const ds = parseDicom(buf);
const v = ds.get("00100010")?.value; // Patient's Name (PN)
if (v?.kind === "personName") console.log(v.values[0]?.alphabetic.familyName);
Returns
Item
One sequence item. Inherits fileMeta (always undefined for nested
items), warnings, and the protected element map from Dataset.
Item.get(...) / Item.has(...) and the rest come from the Dataset
superclass.
Example
import { Item } from "@cosyte/dicom";
// The parser constructs items as follows:
// const item = new Item({ index: 0, warnings: [], elements: new Map() });
Extends
Constructors
Constructor
new Item(
init):Item
Internal
Construct a new structural Item. Producers are the SQ / FFFE
marker parsers.
Parameters
init
ItemInit
Returns
Overrides
Properties
_elements
protectedreadonly_elements:ReadonlyMap<string,Element>
Internal
Element map, keyed by uppercase 8-hex tag. Reached publicly through the
navigation surface; kept protected here so only subclasses (Item, and
any later extension) can introspect it directly.
Inherited from
fileMeta
readonlyfileMeta:FileMeta|undefined
Inherited from
fileOffset
readonlyfileOffset:number|undefined
Byte offset of this Item's (FFFE,E000) Item tag in the Part 10 file it
was read from, counted from the first byte of the File Preamble, which is
how PS3.3 2026d Table F.3-3 counts a DICOMDIR's offsets. For a file read
without a preamble, the 132 bytes of File Preamble and DICM prefix it
lacks are still counted, so the File Meta group's first byte is offset 132.
undefined for an Item with no position in a file: one inside a Deflated
Data Set (a byte of a deflated stream is not a byte of the file), and one
built by hand. deidentify() carries it from each source Item to the Item
it rebuilds, as that Item's identity: it is how serializeDicom ties a
DICOMDIR offset to the Directory Record it named.
Example
import { parseDicom } from "@cosyte/dicom";
const ds = parseDicom(buf);
const first = ds.get("00041220")?.items?.[0];
first?.fileOffset; // where that Directory Record Item starts in the file
index
readonlyindex:number
warnings
readonlywarnings: readonlyDicomParseWarning[]
Inherited from
Accessors
directory
Get Signature
get directory():
DicomDirectory|undefined
The DICOMDIR Directory Record tree, or undefined when this Dataset is not
a DICOMDIR (its File Meta Media Storage SOP Class UID is not
1.2.840.10008.1.3.10). Read from the four offset attributes (PS3.3 2026d
Table F.3-3): root is the record (0004,1200) names and each
(0004,1400) successor, and each record's lowerLevel is the record its
(0004,1420) names and each successor. An offset resolves only when it is
exactly the file offset of an Item of the Directory Record Sequence
(0004,1220); one that is not names no record, and parseDicom warns
(DICOM_DIRECTORY_OFFSET_UNRESOLVED, DICOM_DIRECTORY_OFFSET_MALFORMED,
DICOM_DIRECTORY_RECORD_REVISITED, DICOM_DIRECTORY_OFFSET_DEFLATED).
Memoised on first read.
Limits: record keys are not checked against PS3.3 F.5, the File-set
Consistency Flag and (0004,1202)'s agreement with the end of the root
chain are not checked, and under Deflated Explicit VR LE no offset resolves,
so every record is listed in records and none is linked.
Example
import { parseDicom } from "@cosyte/dicom";
const dir = parseDicom(buf).directory;
for (const patient of dir?.root ?? []) {
for (const study of patient.lowerLevel) {
for (const series of study.lowerLevel) {
for (const image of series.lowerLevel) console.log(image.referencedFileId);
}
}
}
Returns
DicomDirectory | undefined
Inherited from
image
Get Signature
get image():
ImageView
Pixel-interpretation + geometry view. Surfaces exactly
what a renderer needs without guessing: rescaleSlope/signed/
photometricInterpretation stay absent rather than defaulted, and the
three pixel-spacing tags are distinct. Memoised on first read.
Example
import { parseDicom } from "@cosyte/dicom";
const img = parseDicom(buf).image;
img.rescaleSlope; // undefined ⇒ MUST NOT assume 1
Returns
Inherited from
patient
Get Signature
get patient():
PatientView
Patient-identity view. Fail-safe typed-absent fields; id is
not globally unique - match on the {id, issuerOfId, ...} tuple
plus otherIds, never on a bare (0010,0020). Memoised on first read.
Example
import { parseDicom } from "@cosyte/dicom";
const p = parseDicom(buf).patient;
p.name?.alphabetic.familyName; // structured PN, never flattened
Returns
Inherited from
series
Get Signature
get series():
SeriesView
Series-identity & co-registration view. A shared
frameOfReferenceUid means images are spatially co-registered.
Memoised on first read.
Example
import { parseDicom } from "@cosyte/dicom";
const s = parseDicom(buf).series;
s.modality; // "CT"
Returns
Inherited from
study
Get Signature
get study():
StudyView
Study-identity view. instanceUid is the cross-system study
key; accessionNumber ties it to the order. Memoised on first read.
Example
import { parseDicom } from "@cosyte/dicom";
const s = parseDicom(buf).study;
s.instanceUid; // "1.2.840.113619..."
Returns
Inherited from
Methods
elements()
elements(): readonly
Element[]
All elements in this dataset, in parse (insertion) order.
Returns
readonly Element[]
Example
import { parseDicom } from "@cosyte/dicom";
const ds = parseDicom(buf);
for (const el of ds.elements()) console.log(el.tag, el.vr);
Inherited from
get()
get(
tag):Element|undefined
Look up a single element by tag. Tags are normalised to 8-char
uppercase hex, so "7fe00010" and "7FE00010" resolve to the same
element. Returns undefined when the tag is absent.
Parameters
tag
string
Returns
Element | undefined
Example
import { parseDicom } from "@cosyte/dicom";
const ds = parseDicom(buf);
const rows = ds.get("00280010"); // Rows
if (rows?.value.kind === "numbers") console.log(rows.value.values[0]);
Inherited from
getAll()
getAll(
tag): readonlyElement[]
All elements matching a tag as an array (never undefined). A dataset
holds at most one element per tag, so this returns a 0- or 1-length
array - the convenience complement of Dataset.get for callers
that prefer an always-array shape.
Parameters
tag
string
Returns
readonly Element[]
Example
import { parseDicom } from "@cosyte/dicom";
const ds = parseDicom(buf);
for (const el of ds.getAll("00080060")) console.log(el.value);
Inherited from
has()
has(
tag):boolean
true when an element with the given tag is present (case-insensitive).
Parameters
tag
string
Returns
boolean
Example
import { parseDicom } from "@cosyte/dicom";
const ds = parseDicom(buf);
if (ds.has("00100010")) console.log("has Patient's Name");
Inherited from
ProfileDefinitionError
Thrown by defineProfile() when the supplied options are invalid.
Example
import { defineProfile, ProfileDefinitionError } from "@cosyte/dicom";
try {
defineProfile({ name: "" });
} catch (err) {
if (err instanceof ProfileDefinitionError) {
console.error(err.message); // "Profile name must be a non-empty string."
}
}
Extends
Error
Constructors
Constructor
new ProfileDefinitionError(
message,profileName?):ProfileDefinitionError
Parameters
message
string
Actionable description of what made the options invalid.
profileName?
string
The offending profile's name, when known.
Returns
Overrides
Error.constructor
Properties
profileName?
readonlyoptionalprofileName?:string
The offending profile's name when known (undefined when the failure
is the name itself). Lets callers attribute a composed-lineage failure to
the specific profile that introduced it.
Sequence
One SQ (Sequence) element's structural body.
length is the on-wire length: a real byte count for defined-length
SQ, or 0xFFFFFFFF (4_294_967_295) when undefined-length.
Example
import { Sequence } from "@cosyte/dicom";
// The parser constructs sequences as follows:
// const sq = new Sequence([item0, item1], 0xFFFFFFFF);
Constructors
Constructor
new Sequence(
items,length):Sequence
Internal
Construct a new structural Sequence. The items array is frozen
at the constructor boundary so downstream mutation cannot escape.
Parameters
items
readonly Item[]
length
number
Returns
Properties
items
readonlyitems: readonlyItem[]
length
readonlylength:number
Interfaces
CodedConcept
A coded triplet (PS3.16 §8, Table 8-1): Code Value (0008,0100),
Coding Scheme Designator (0008,0102), Code Meaning (0008,0104). The
canonical scheme OID is resolved only for the four standard designators;
legacy SNOMED designators (SRT/SNM3/99SDM) deliberately resolve to
undefined because their code values differ from SCT (CP-730).
Example
import type { CodedConcept } from "@cosyte/dicom";
const c: CodedConcept = {
codeValue: "C-B1003",
codingSchemeDesignator: "SCT",
codeMeaning: "Hounsfield unit",
schemeUid: "2.16.840.1.113883.6.96",
};
Properties
codeMeaning?
readonlyoptionalcodeMeaning?:string
codeValue?
readonlyoptionalcodeValue?:string
codingSchemeDesignator?
readonlyoptionalcodingSchemeDesignator?:string
schemeUid?
readonlyoptionalschemeUid?:string
DateParts
The calendar components a temporal value actually stated, and nothing else.
Every value is a number, month is 1 to 12 (never the JS Date 0 to 11),
and a component the value did not state is ABSENT rather than present and
undefined: there is no precision key because the key set is the
precision. No raw, no valid and no parse bookkeeping reaches it.
The shape is the one Temporal.PlainDateTime.from and luxon's
DateTime.fromObject accept, which is why the names are singular where
DicomTime and DicomDateTime spell them plurally. Delete
offsetMinutes and either constructor takes the rest with no key rename and
no value adjustment. That is a documented property rather than a tested one:
proving it would mean taking a dependency, and this package takes none for it.
Example
import { parseDateTime, toObject } from "@cosyte/dicom";
import type { DateParts } from "@cosyte/dicom";
const parts: DateParts | undefined = toObject(parseDateTime("202401151330").value);
Object.keys(parts ?? {}); // ["year", "month", "day", "hour", "minute"]
Properties
day?
readonlyoptionalday?:number
Day of month, 1 to 31.
hour?
readonlyoptionalhour?:number
Hour of day, 0 to 23.
millisecond?
readonlyoptionalmillisecond?:number
The first three digits of the stated fraction, right-padded with zeroes.
minute?
readonlyoptionalminute?:number
Minute, 0 to 59.
month?
readonlyoptionalmonth?:number
Calendar month, 1 to 12.
offsetMinutes?
readonlyoptionaloffsetMinutes?:number
Signed minutes east of UTC, present only when the value carried an offset.
second?
readonlyoptionalsecond?:number
Second, 0 to 59.
TM and DT permit 60 for a leap second and the decoders keep it, but a
value stating it is not projected: there is no ISO rendering of it a reader
does not move, and no instant to build. See the module doc block.
year?
readonlyoptionalyear?:number
Calendar year as written, so 0050 is the year 50 and never 1950.
DefineProfileOptions
Options accepted by defineProfile. Only name is required; every
other field defaults to empty. With exactOptionalPropertyTypes, omit an
unset key rather than passing undefined.
Example
import { defineProfile, WARNING_CODES } from "@cosyte/dicom";
const lenientCd = defineProfile({
name: "lenient-cd",
description: "Tolerant of conformance-loose archive CDs",
suppress: [WARNING_CODES.DICOM_ODD_LENGTH_VALUE_PADDED],
});
Properties
description?
readonlyoptionaldescription?:string
escalate?
readonlyoptionalescalate?: readonlyWarningCode[]
extends?
name
readonlyname:string
privateTags?
readonlyoptionalprivateTags?:Readonly<Record<string,Readonly<Record<string,PrivateTagDefinition>>>>
suppress?
readonlyoptionalsuppress?: readonlyWarningCode[]
DeidentificationMethodCode
One row of PS3.16 CID 7050 "De-identification Method": the three Basic Coded Entry Attributes (PS3.3 2026d Table 8.8-1a) of one code, exactly as the context group spells them.
deidentify() writes each code it records as one Item of
(0012,0064) De-identification Method Code Sequence carrying Code Value
(0008,0100), Coding Scheme Designator (0008,0102) and Code Meaning
(0008,0104), and nothing else. Coding Scheme Version is omitted because PS3.3
requires it only where the designator is not sufficient to identify the code,
and DCM is.
Example
import { DEIDENTIFICATION_METHOD_CODES, type DeidentificationMethodCode } from "@cosyte/dicom";
const profile: DeidentificationMethodCode = DEIDENTIFICATION_METHOD_CODES.profile;
profile.codeValue; // "113100"
Properties
codeMeaning
readonlycodeMeaning:string
Code Meaning (0008,0104), a VR LO value, spelled as CID 7050 spells it.
codeValue
readonlycodeValue:string
Code Value (0008,0100), a VR SH value.
codingSchemeDesignator
readonlycodingSchemeDesignator:"DCM"
Coding Scheme Designator (0008,0102). Every CID 7050 row is DCM.
DeidentifiedAttribute
One audited attribute outcome. tag, keyword, action, applied and
repeatingGroup carry never a decoded value: keyword, action and
repeatingGroup come from the Part 6 and Annex E tables, and tag is bound to
a tag those tables carry a row for, which is membership in a closed table
rather than a shape test.
🩺 contextPath is NOT in that class and this docstring used to say it was.
It is a chain of tags read off the wire, bound by nothing - see
DeidentifiedAttribute.contextPath and the note on
DeidentifyReport.
Example
import { deidentify, parseDicom, type DeidentifiedAttribute } from "@cosyte/dicom";
const { report } = deidentify(parseDicom(buf));
report.attributes.forEach((a: DeidentifiedAttribute) => {
console.log(a.keyword, a.action, a.applied); // composed from tables - safe to log
});
Properties
action
readonlyaction:"D"|"Z"|"X"|"K"|"C"|"U"
The resolved single action after collapsing any conditional code.
applied
readonlyapplied:AppliedAction
contextPath?
readonlyoptionalcontextPath?: readonlystring[]
Tag/index chain for an attribute inside a sequence; omitted at the root.
🩺 Each segment is TAG[index], and the TAG half is read off the wire
with no table behind it. It is whatever tag the descent walked, so it is
bound by neither a shape test nor membership in a closed one - which is what
separates it from tag, keyword, action and repeatingGroup. On a file
where an under-declared Value Length desynchronized the reader onto four
bytes sitting inside somebody's value, those four bytes become a segment
here. It is not the only identifier on this report read off the wire -
DeidentifyReport.removedPrivateTags and
UnauditableSequenceFinding.tag are too - but those two are disclosed
as such, and this one was documented as structural.
Measured on a synthetic LO carrier holding "MRS BRAIN SMITHSON" that
under-declares by four: the reader resynchronizes onto a fabricated SQ
header, descends it, and the report reads contextPath: ["53484E4F[0]"] -
"HSON" in wire order, recovered by writing the two halves back with
writeUInt16LE. No warning is raised and every finding array on the
report is empty. Change the surname and the published segment changes with
it. PRE-EXISTING, on every release that has shipped the field.
🛑 IT IS NOT THE ONLY PLACE THOSE BYTES SURFACE, AND AN EARLIER DRAFT OF
THIS NOTE SAID IT WAS. A GRADED PASS REFUTED THAT AND IT MUST NOT COME
BACK. On the same file the de-identified Dataset still carries the
fabricated (5348,4E4F), so serializeDicom writes its header back out in
full - "HSON" included - inside an object stamped
(0012,0062) Patient Identity Removed = YES. That re-emission belongs to
the disclosed under-declare carrier class, not to this field, and neither is
a bound on the other: redacting contextPath from a log does not make the
object safe to share.
That fixture no longer reproduces either half. (5348,4E4F) has no row
in this build's PS3.6 registry and none in Table E.1-1, so deidentify() now
removes the fabricated Sequence whole before it reads the VR, and neither
this field nor the object carries "HSON" (see
UnregisteredElementRemoval); that is what
test/integration/phi-diagnostic-surface.test.ts pins now. It is a fact
about that shape and not a bound on this field: a Sequence the run does
descend still contributes whatever tag the wire gave it.
It is published anyway, on the same footing as DeidentifyReport.removedPrivateTags: where an attribute sat is the whole audit value of the field, and withholding it would destroy that on every well-formed file in order to bound a malformed one. Treat it as PHI when the source is untrusted.
keyword
readonlykeyword:string
repeatingGroup?
readonlyoptionalrepeatingGroup?:string
Present when the action came from a Table E.1-1 row that names a
repeating-group family rather than this single tag: the mask that matched,
e.g. "60xx4000" for Overlay Comments in any overlay plane. tag is always
the concrete tag that was in the file. Absent for every exact-tag row.
tag
readonlytag:string
DeidentifyOptions
Options controlling a de-identification run. All optional - the default is the Basic Application Level Confidentiality Profile with no Retain options.
Example
import { deidentify, parseDicom, type DeidentifyOptions } from "@cosyte/dicom";
const opts: DeidentifyOptions = { retain: ["RetainLongitudinalTemporal", "CleanDescriptors"] };
const { dataset } = deidentify(parseDicom(buf), opts);
Properties
deidentificationMethod?
readonlyoptionaldeidentificationMethod?:string
Text added to (0012,0063) De-identification Method. Default names the
Basic Profile and the active options.
It never changes (0012,0064). The CID 7050 codes the run writes there
follow the options that ran, and free text cannot alter what ran; see
DEIDENTIFICATION_METHOD_CODES.
The default is multi-valued, one Value per name, and that is a conformance
fix rather than a style choice. PS3.5 2026c Table 6.2-1 caps an LO at
64 characters per Value, and (0012,0063) is 1-n; the single-value
default this replaced measured 76 characters with no options and 272 with
every option name published at that time, so every run this library ever
made wrote a value no LO may legally carry. The Profile name is now one
Value of 61 characters and each active option is its own, so no option
subset can exceed the maximum.
Your string is not bounded here, but it is no longer silent. A value of
your own longer than 64 characters is written through as given - it is
yours, and splitting or truncating it would invent a record you did not
write - and report.warnings carries
DICOM_DEIDENT_METHOD_VALUE_OVER_LENGTH to say so. Split it on `` yourself
if a strict receiver is in your path. The same code covers an over-long
value the source file wrote and this run kept, which in the common case
is this library's own 76-character text from a release before the default
became multi-valued. The measurement is over bytes: a Value of 64 bytes
or fewer can never carry more than 64 characters, so it cannot miss one that
is genuinely over, but PS3.5 §6.2 specifies the bound in characters rather
than bytes and excludes Code Extension escape sequences from the count, so it
raises on any conformant Value whose bytes outnumber its counted characters.
PS3.15 E.1.1 says this string is "inserted in or added to" the attribute, so
a value the incoming Data Set already carried is kept and this one is
appended after a \ as a further value of the 1-n attribute - the
provenance chain, not a replacement.
This 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, not content (PS3.5 Table 6.2-1's LO
row, which describes a Value - and LO is 1-n). They are ignored when
a value here is matched against one already recorded, per value, on both
sides, and they are trimmed from the value written. That makes repeated
de-identification 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.
A string that is padding only records nothing. Leading spaces are yours and
are written through untouched.
One bound, and it is over the value that would be written, not over the
join: when that value would exceed the largest Value Length an LO can
encode, the prior value is replaced rather than added to - an element the
serializer cannot encode would take the whole de-identified object down - and
report.warnings carries DICOM_DEIDENT_METHOD_NOT_ADDED. That includes a
prior value already at the ceiling which records this method already, where
there is no join to exceed anything. A string longer than that ceiling on its
own is not bounded here and will fail to serialize, exactly as it did before
this option grew a join.
DICOM_DEIDENT_METHOD_NOT_ADDED means "the length ceiling was reached", never
"every fallback is disclosed": a (0012,0063) a file encoded under a VR other
than LO is also replaced, and that one raises DICOM_DEIDENT_METHOD_NOT_LO.
Two codes rather than one because the causes are unrelated - the chain outgrew
the VR, or the bytes were never in that VR at all - and a prior value that is
empty or padding only raises neither, because nothing was lost.
When the prior value is kept, report.warnings carries
DICOM_DEIDENT_METHOD_PRIOR_RETAINED: (0012,0063) is not in Table E.1-1, so
nothing in the run inspected or redacted those bytes, and a name a sender
wrote there is in output stamped (0012,0062) = YES. That code discloses the
retention only; the length of what is written, from whatever source,
is disclosed by DICOM_DEIDENT_METHOD_VALUE_OVER_LENGTH and by nothing else.
profile?
readonlyoptionalprofile?:Profile
A Profile whose private-dictionary overlay names the
known-safe private attributes to keep when RetainSafePrivate is active.
Not the only way an attribute is known safe, and no longer required for
RetainSafePrivate to keep anything. PS3.15 2026c §E.3.10 lists four ways
that knowledge may be established and a profile is two of them
(Conformance Statement documentation, and "some other means"); the first is
the file's own Private Data Element Characteristics Sequence
(0008,0300), which deidentify() reads when this Option is active whether
or not a profile is passed. So a file from a vendor you hold no profile for
keeps the private attributes that file declares non-identifying, and
passing a profile only ever adds to that: a profile retention survives a
block the declaration calls UNSAFE or says nothing about.
🩺 What the declaration route retains rests on the SENDER's assertion.
The system that wrote the file said those blocks carry no identifying
information; nothing here re-derives it, and an opaque value in a block
declared SAFE reaches de-identified output unexamined. If you do not trust
the sender, leave RetainSafePrivate off - §E.3.10's own alternative, and
the only mitigation the standard offers.
With neither this nor a declaration in the file, RetainSafePrivate keeps
nothing (fail-safe).
retain?
readonlyoptionalretain?: readonlyDeidentifyOption[]
Annex E option sets to activate (Retain* / Clean*). Default: none.
uidMap?
readonlyoptionaluidMap?:Map<string,string>
A caller-owned source→replacement UID cache. Pass one shared map across a whole study/archive to make UID remapping consistent by construction even across separate calls (it is consistent anyway - the mapping is content- derived - but a shared map also makes repeats O(1)).
uidRoot?
readonlyoptionaluidRoot?:string
Root for generated UIDs (action U). Default "2.25".
DeidentifyReport
The audit trail returned alongside the de-identified dataset.
Most fields are composed from static tables: Part 6 keywords, Annex E action codes, the active option names. Several fields are not, and they are named here rather than in a footnote. 🛑 Do not quote a COUNT of them, here or anywhere else. The count read "one" and then "two" and then "three", and was wrong every time it was read, because each correction bumped the numeral without re-deriving the list. The list below carried its own numerals until one had to be added, which is the same disease one step removed, so they are gone. Treat it as the current reading of a surface that has grown, not as a proof of exhaustiveness.
uidMap- its keys are the source UIDs read out of the file, kept so a caller can make UID replacement consistent across a study or an archive. A Study or SOP Instance UID is a unique identifying number, so treat it as PHI.removedPrivateTags- see the field's own note. On a well-formed file these are the sender's own private tag numbers and carry nothing; on a malformed one a tag can be four bytes of a value, and it is measured, not theoretical.unauditableSequences[].tag- see UnauditableSequenceFinding. Same shape asremovedPrivateTagsby a narrower route: a private carrier a Profile vouched for and this run emptied is named there, and an under-declared length upstream can resynchronize the reader onto four bytes that spell such a block. The package's usual answer to a fabricated header,undefinedVrElements, carries a byte offset and no tag, and still answers it whenever the fabricated VR is outside the 34 PS3.5 §6.2 defines. It cannot when the fabricated VR is one of them, because those two files are byte-identical.unenumerablePrivateRemovals[].tag- see UnenumerablePrivateRemoval. The same four bytes by the same route, on the record of the removal rather than of an emptying, and this one is uncapped because it is the record of an action rather than a diagnostic.undefinedVrElements[].byteLengthandunauditableSequences[].byteLength- the declared Value Length read off the element header, so on a fabricated header it is four document bytes wearing a decimal:"SO\0\0"publishes20307, two letters of a surname, put back with onereadUInt32LE. They JOINED this list rather than always having been on it. Through0.0.14the twoDICOM_DEIDENT_*_NOT_AUDITABLEmessages rendered the same number; binding it out of those factory signatures left these model fields as its only publisher, which is a smaller surface and not a closed one. Narrowing them is a product call rather than a defect fix: a bound empties the field on every well-formed file, where the number is exactly the audit information it exists to carry.group0004Removals[].tag- see Group0004Removal. The same four bytes by the same route asremovedPrivateTags, on the record of a removal PS3.15 §E.1.1 requires. On a well-formed file these are PS3.6's registered DICOMDIR directory-structuring tags and carry nothing.fileMetaElementsDropped[].tagand.vr- see FileMetaDroppedElement. Two bytes rather than four: the File Meta pre-pass stops at the first non-0002group, so the group half of the tag is fixed and only the element number is free, and the VR is two more bytes the same desynchronized read would supply..byteLengthjoinsundefinedVrElements[].byteLengthin the entry above by the same route.contextPath, on every finding that carries one - see DeidentifiedAttribute.contextPath, which holds the measurement. The segment tags come off the wire with no table behind them, so a fabricatedSQheader the reader descended is named there,PRE-EXISTING, with no warning and no finding array to correlate it with. This is the field the rest of this docstring, the tolerance table and the troubleshooting guide all called structural. It is a logging hazard and nothing more: on that same file the de-identified object itself re-emits the fabricated header, so redacting this field does not make the object safe.
embeddedAttributes[].hidden left that list in
DICOM-DIAGNOSTIC-PHI-RESIDUALS - an entry is now a literal PS3.15 Table
E.1-1 row, so it is not value-bearing. Its own disclosure had been reworded
twice by then, and this repo deletes a disclosure at that point rather than
writing a third; what is true of the field is stated once, on
EmbeddedAttributeFinding. The field is still uncapped.
So "the report is safe to log apart from uidMap" is not an accurate
description of this type, and was corrected rather than kept convenient.
This is the only copy of that list. Two others lived in module docstrings,
still naming hidden after it left and never naming contextPath at all; a
graded pass found them and they were deleted rather than resynced.
Example
import { deidentify, parseDicom, type DeidentifyReport } from "@cosyte/dicom";
const { report }: { report: DeidentifyReport } = deidentify(parseDicom(buf));
console.log(report.attributes.length, "attributes acted on");
console.log(report.warnings.map((w) => w.code)); // e.g. burned-in annotation
Properties
attributes
readonlyattributes: readonlyDeidentifiedAttribute[]
Per-attribute outcomes for every attribute Annex E acted on.
embeddedAttributes
readonlyembeddedAttributes: readonlyEmbeddedAttributeFinding[]
Values emptied because whole Data Elements were encoded inside them by an over-declared Value Length. Empty on a well-formed file; a non-empty array means the source was malformed in a way that hid attributes from the action table, so treat it as a data-quality alarm on the sender as well as an audit line. See EmbeddedAttributeFinding.
fileMetaElementsDropped
readonlyfileMetaElementsDropped: readonlyFileMetaDroppedElement[]
Non-modeled (0002,xxxx) File Meta elements this run dropped instead of
re-emitting, because the de-identified File Meta group describes this
de-identifying application rather than the source (PS3.15 §E.1.1). Empty
when the source group held only modeled elements, and empty when the source
parsed with no File Meta group at all.
This is the record of a deliberate fidelity loss, not of a defect. The
byte-for-byte File Meta round trip FileMeta.extraElements documents is
scoped to parse-then-serialize and does not hold here.
Capped; the count beside it is not. See FileMetaDroppedElement and DeidentifyReport.fileMetaElementsDroppedCount.
fileMetaElementsDroppedCount
readonlyfileMetaElementsDroppedCount:number
How many non-modeled (0002,xxxx) elements this run dropped, complete at
any input size.
The field exists because its sibling array is capped: a caller that needs to count the fidelity loss cannot get the number from an array whose length saturates, and re-parsing the output does not give it either - the elements are gone from the output by design. One number cannot be inflated by an attacker-chosen element count, so bounding it would buy nothing.
group0004RemovalCount
readonlygroup0004RemovalCount:number
How many (0004,xxxx) elements this run removed, complete at any input
size, for the reason
DeidentifyReport.fileMetaElementsDroppedCount states.
It is also the field that tells the two §E.1.1 rules apart at a glance: this one counts group-0004 removals, the other counts File Meta drops, and neither can be read off the other.
group0004Removals
readonlygroup0004Removals: readonlyGroup0004Removal[]
Data Elements whose Group Number is 0004, removed at every depth this
run reached, as PS3.15 §E.1.1 requires "from any SOP Instance or DICOM File
other than a DICOMDIR File".
Empty when the object carried none, and empty when it declared Media Storage
SOP Class UID 1.2.840.10008.1.3.10 - the carve-out, which is disclosed on
DeidentifyReport.warnings as
DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED rather than left to be
inferred from an empty array.
Capped; the count beside it is not. See Group0004Removal and DeidentifyReport.group0004RemovalCount.
removedPrivateTags
readonlyremovedPrivateTags: readonlystring[]
Private tags removed (kept ones are omitted) - every one of them, whichever rule removed it: the Basic Profile's default removal, a reservation the file did not settle, and, since the unenumerable class became a removal, a private attribute this run did not enumerate. Its meaning is unchanged and it is still uncapped; what it does not carry is a reason, which is why the unenumerable removals are also recorded on DeidentifyReport.unenumerablePrivateRemovals, where they can be told apart from the rest.
This is the second field that is not value-free, and the qualification is
measured. A tag here is composed from four bytes of the source, and on a
file whose Value Lengths disagree with its bytes those four bytes can be
document content rather than a tag the sender wrote: an OB carrier holding
"SECRET-NOTE-" followed by a well-formed odd-group header reports
["41534342"], whose wire-order bytes read "SABC". That reproduces
identically on every release that has shipped this field.
It is reported anyway, because which private tags were removed is the whole audit value of the field and withholding them would destroy it on every well-formed file to bound a malformed one. Narrowing it - to blocks a creator in the same Data Set actually reserved, say - is a product decision about audit value versus a four-byte echo, not a defect fix, and it has not been made. Treat this array as PHI when the source is untrusted.
retained
readonlyretained: readonlyDeidentifyOption[]
The Retain/Clean options that were active for this run.
Not a list of what survived. Registered attributes Table E.1-1 does not
list are kept without appearing anywhere in this field - (0012,0063)
De-identification Method and the prior Items of (0012,0064)
De-identification Method Code Sequence are the ones whose retention is
disclosed, as DICOM_DEIDENT_METHOD_PRIOR_RETAINED and
DICOM_DEIDENT_METHOD_CODES_PRIOR_RETAINED on
DeidentifyReport.warnings, because neither is an option set.
uidMap
readonlyuidMap:ReadonlyMap<string,string>
Source UID → replacement UID, for cross-file consistency. The keys are document values, not composed identifiers: this is the one field of the report that carries PHI.
unauditableSequences
readonlyunauditableSequences: readonlyUnauditableSequenceFinding[]
Carriers whose nested Data Sets this run could not reach and emptied for
it: an SQ whose items the parser did not materialize, so the run had no
Data Sets to walk and could not discharge PS3.15 §E.1.1's obligation inside
them, and a private carrier a Profile declares SQ that the parse
tree resolved otherwise. Content was dropped from the de-identified output,
and for the first producer the matching DICOM_SQ_NOT_DESCENDED entry on
Dataset.warnings says why the parse refused.
🛑 THIS ARRAY NO LONGER PRODUCES applied: "kept", AND THAT IS AN
AUDIT-CONTRACT CHANGE. A private attribute retained under
RetainSafePrivate whose value this run never enumerated used to be listed
here, kept verbatim, with the array saying so. That instance is removed
now and recorded on
DeidentifyReport.unenumerablePrivateRemovals. An entry here has one
meaning again: this content is not in your output.
Empty on a well-formed file, including one whose private attributes a profile vouches for. It stopped being populated by ordinary conformant files when the retained class left it.
Capped, and the cap is on the record only. A crafted input can carry
tens of thousands of un-auditable elements, so this array (and its matching
warnings) stops at MAX_UNAUDITABLE_SEQUENCE_FINDINGS. Every un-auditable
carrier is still emptied whether or not it is listed; an array exactly that
long means "at least this many", so read it as truncated rather than as a
total. The unenumerable removals are budgeted apart from it and their
record is not capped at all.
It covers private sequences too, since DICOM-PRIVATE-SQ-CARVE-OUT. A
private SQ a Profile vouches for under RetainSafePrivate used to
be kept verbatim and never appear here; it is now emptied and listed on the
same terms as any other, because the profile's licence under PS3.15 §E.3.10
is over a private attribute and not over an item stream nothing could read.
undefinedVrElements
readonlyundefinedVrElements: readonlyUndefinedVrFinding[]
Elements emptied because their on-wire VR is not one of the 34 PS3.5 §6.2 defines, so their bytes are not a Value Field this library decoded and PS3.15 §E.1.1's obligation over what is inside them could not be discharged. Empty on a file conformant to PS3.5 2026c: a sender that writes one of the 34 VRs that edition defines never produces one, and an Implicit VR LE file cannot - there the VR comes from the dictionary. The edition is not pedantry: §6.2 exists precisely to say how a future VR will be encoded, so a file conformant to a later edition using a newly defined VR is the population that sentence exists for. (What such a file does on this library's parse path is not summarized here - it was measured and the shapes disagree.) A non-empty array means the source desynchronized the reader, usually by under-declaring a Value Length somewhere earlier. See UndefinedVrFinding.
Capped, and the cap is on the record only, exactly as
DeidentifyReport.unauditableSequences is: a crafted 1 MiB input can
carry over a hundred thousand such elements, so this array and its matching
warnings stop at MAX_UNDEFINED_VR_FINDINGS. Every one of them is still
emptied; an array exactly that long means "at least this many".
A finding here names a byte offset, not a tag - uniquely among the report's findings, and for a reason worth reading in UndefinedVrFinding: the tag of a fabricated header is itself part of some element's value.
This list has no carve-out, and the reason is structural rather than a
promise: keepOrEmpty is the only path that writes a source value into
de-identified output unchanged, and the test sits at the top of it. Every
other outcome - X remove, Z/C empty, D dummy, U remap, and a
private tag the Basic Profile drops - already replaces the value. So a
RetainSafePrivate element a Profile vouches for still reaches this
test and is still emptied. Its sibling
DeidentifyReport.unauditableSequences had a real one until
DICOM-PRIVATE-SQ-CARVE-OUT; neither has one now, and they arrive at that by
different routes - this one because nothing bypasses keepOrEmpty, that one
because a vouched-for private SQ is routed into the ordinary SQ branches.
unenumerablePrivateRemovals
readonlyunenumerablePrivateRemovals: readonlyUnenumerablePrivateRemoval[]
Private attributes removed because this run did not enumerate their
value - one entry per instance, naming the tag, the outcome ("removed"),
the reason ("unenumerable") and the Data Set it lived in.
This is the surface the fail-safe's audit half is built on, and it is the
one array on this report that is never capped or truncated. A caller can
separate, from this record alone and for every such removal at any input
size, the attributes removed for being unenumerable from the attributes
removed by the Annex E action table (report.attributes) and from the ones
whose value was emptied. The matching warnings ARE bounded, so past their
cap this record is the only complete account and the diagnostics are the
bounded one.
Empty unless RetainSafePrivate plus a Profile is active, because
without both nothing private is retained far enough to be judged: the Basic
Profile removes every private attribute and names it in
DeidentifyReport.removedPrivateTags instead. A profile lookup that
misses is that case too, and never appears here. The file's own
(0008,0300) declaration does not populate it either, and that is not an
omission: a value that declaration covers is one §E.3.10 says the run knows
about, so it is retained rather than reaching this rule at all.
See UnenumerablePrivateRemoval for what counts as enumeration, for the over-redaction this costs, and for why it names an instance rather than a tag.
unregisteredElementRemovalCount
readonlyunregisteredElementRemovalCount:number
How many elements this run removed under the rule
DeidentifyReport.unregisteredElementRemovals records, complete at
any input size, for the reason
DeidentifyReport.fileMetaElementsDroppedCount states. 0 on a run
that removed none.
unregisteredElementRemovals
readonlyunregisteredElementRemovals: readonlyUnregisteredElementRemoval[]
Non-private Data Elements removed at every depth this run reached because this build's PS3.6 2026d registry does not carry their tag and Table E.1-1 does not list it. See UnregisteredElementRemoval for the rule, for what it costs on a newer-edition file, and for why an entry names a byte offset and no tag.
Empty on a run that removed none. A separate record from DeidentifyReport.attributes, DeidentifyReport.removedPrivateTags, DeidentifyReport.group0004Removals and DeidentifyReport.undefinedVrElements: an element removed here is on none of those.
Capped; the count beside it is not. See DeidentifyReport.unregisteredElementRemovalCount.
warnings
readonlywarnings: readonlyDicomParseWarning[]
Safety warnings - notably burned-in-pixel annotation that cannot be cleaned.
DeidentifyResult
The result of deidentify: a new dataset plus its audit report.
Example
import { deidentify, parseDicom, serializeDicom, type DeidentifyResult } from "@cosyte/dicom";
const { dataset, report }: DeidentifyResult<ReturnType<typeof parseDicom>> = deidentify(parseDicom(buf));
const safe = serializeDicom(dataset); // input dataset is never mutated
void report;
Type Parameters
TDataset
TDataset
Properties
dataset
readonlydataset:TDataset
report
readonlyreport:DeidentifyReport
DicomDate
A tolerantly-decoded DA (Date) value. raw always carries the on-wire
string (PHI-safe - a date is not an identifier on its own, but is
preserved verbatim regardless). valid is true only when the string
parsed cleanly to a YYYYMMDD calendar date; otherwise the numeric
fields are omitted and raw is the source of truth.
Example
import type { DicomDate } from "@cosyte/dicom";
const d: DicomDate = { raw: "20240115", valid: true, year: 2024, month: 1, day: 15 };
Properties
day?
readonlyoptionalday?:number
month?
readonlyoptionalmonth?:number
raw
readonlyraw:string
valid
readonlyvalid:boolean
year?
readonlyoptionalyear?:number
DicomDateTime
A tolerantly-decoded DT (DateTime) value (PS3.5 §6.2). offsetMinutes
is the signed UTC offset (&ZZXX suffix) in minutes when present.
Example
import type { DicomDateTime } from "@cosyte/dicom";
const dt: DicomDateTime = { raw: "20240115133015", valid: true, year: 2024, month: 1, day: 15, hours: 13, minutes: 30, seconds: 15 };
Properties
day?
readonlyoptionalday?:number
fractionalSeconds?
readonlyoptionalfractionalSeconds?:number
hours?
readonlyoptionalhours?:number
minutes?
readonlyoptionalminutes?:number
month?
readonlyoptionalmonth?:number
offsetMinutes?
readonlyoptionaloffsetMinutes?:number
raw
readonlyraw:string
seconds?
readonlyoptionalseconds?:number
valid
readonlyvalid:boolean
year?
readonlyoptionalyear?:number
DicomDirectory
The Directory Record tree of a DICOMDIR, read from its offsets.
records is every Item of the Directory Record Sequence, in order, whether
or not an offset reaches it. root is the root directory entity: the record
(0004,1200) names, then each (0004,1400) successor in order. A record is
reached at most once: an offset that would reach one again is not followed.
Example
import { parseDicom, type DirectoryRecord } from "@cosyte/dicom";
const dir = parseDicom(buf).directory; // undefined unless the object is a DICOMDIR
const walk = (records: readonly DirectoryRecord[], depth = 0): void => {
for (const r of records) {
console.log(" ".repeat(depth), r.type, r.referencedFileId?.join("/"));
walk(r.lowerLevel, depth + 1);
}
};
if (dir !== undefined) walk(dir.root);
Properties
records
readonlyrecords: readonlyDirectoryRecord[]
Every Directory Record, in Directory Record Sequence order.
root
readonlyroot: readonlyDirectoryRecord[]
The root directory entity, in (0004,1400) order.
DicomParseWarning
Data shape for every Tier-2 warning emitted by the parser. Warnings are
plain data (distinct from DicomParseError, which is a thrown Error
subclass) so they can be safely accumulated into Dataset.warnings and
passed to onWarning callbacks.
There is intentionally NO snippet field on warnings:
real-world files routinely produce 50+ warnings and a per-warning
snippet would balloon retained memory. Snippets appear only on
DicomParseError (the strict-mode escalation path).
Example
import type { DicomParseWarning } from "@cosyte/dicom";
const w: DicomParseWarning = {
code: "DICOM_MISSING_PREAMBLE",
message: "No DICM magic at offset 128.",
position: { byteOffset: 0 },
};
Properties
code
readonlycode:WarningCode
message
readonlymessage:string
position
readonlyposition:DicomPosition
DicomPosition
Positional context for a DicomParseWarning or DicomParseError.
🛑 byteOffset IS NOT ALWAYS RELATIVE TO THE SOURCE BUFFER, AND THIS
JSDOC SAID IT WAS. For the Deflated Explicit VR LE transfer syntax,
deflated: true says the offset indexes the inflated dataset buffer rather
than the on-disk source. That flag is the only frame this type carries, and
it is not the only frame this parser has: a defined-length Sequence Item is
parsed from a slice, so a warning raised inside one carries an
item-relative offset with nothing on the position to say so. The same is true
of Element.byteOffset, and has been since the parser was written.
The residual is PRE-EXISTING and is not closed here. What is closed is
the thrown side: DicomParseError.offsetFrame names the coordinate system
from a closed set (see OFFSET_FRAMES), and that covers a Tier-3 fatal and
the { strict: true } escalation of a Tier-2 warning. It does not reach a
warning on the lenient path, which is this type. Do not read the fatal's
frame contract as one this type has.
With exactOptionalPropertyTypes: true, callers should omit unset keys
rather than passing undefined (mirrors @cosyte/hl7 sibling discipline).
Example
import type { DicomPosition } from "@cosyte/dicom";
const p: DicomPosition = { byteOffset: 132, fileMeta: true };
Properties
byteOffset
readonlybyteOffset:number
contextPath?
readonlyoptionalcontextPath?: readonlystring[]
Tag chain for nested SQ items, e.g. ["0040A730", "0", "00080100"]. Omit when at root.
deflated?
readonlyoptionaldeflated?:boolean
True when offset is into the inflated dataset buffer (Deflated TS only). Omit when not applicable.
fileMeta?
readonlyoptionalfileMeta?:boolean
True when offset is inside the File Meta group. Omit (do not pass undefined) when not applicable.
DicomTime
A tolerantly-decoded TM (Time) value (PS3.5 §6.2; max length 14 bytes).
fractionalSeconds is the value after the decimal point as a number in
[0,1) when present.
Example
import type { DicomTime } from "@cosyte/dicom";
const t: DicomTime = { raw: "133015.250000", valid: true, hours: 13, minutes: 30, seconds: 15, fractionalSeconds: 0.25 };
Properties
fractionalSeconds?
readonlyoptionalfractionalSeconds?:number
hours?
readonlyoptionalhours?:number
minutes?
readonlyoptionalminutes?:number
raw
readonlyraw:string
seconds?
readonlyoptionalseconds?:number
valid
readonlyvalid:boolean
DirectoryRecord
One Directory Record of a DICOMDIR: an Item of the Directory Record Sequence
(0004,1220), with the records its offsets name.
Example
import { parseDicom } from "@cosyte/dicom";
const dir = parseDicom(buf).directory;
for (const patient of dir?.root ?? []) {
patient.type; // "PATIENT"
for (const study of patient.lowerLevel) study.item.get("0020000D"); // Study Instance UID
}
Properties
index
readonlyindex:number
The record's 0-based index in the Directory Record Sequence.
item
readonlyitem:Item
The record's own Data Set, for its record keys.
lowerLevel
readonlylowerLevel: readonlyDirectoryRecord[]
The records of this record's lower-level directory entity: the one its
(0004,1420) names, then each (0004,1400) successor in order. Empty when
the offset is zero or names no record.
referencedFileId
readonlyreferencedFileId: readonlystring[] |undefined
The Referenced File ID (0004,1500): its components in order (a path
relative to the File-set root, one component per value), as the file wrote
them less only the padding PS3.5 makes insignificant in CS (the trailing
pad, and each value's leading and trailing spaces). Kept verbatim: never
de-identified, rewritten or joined, and the element's own bytes on item
are untouched. undefined when the record carries none.
type
readonlytype:string|undefined
The Directory Record Type (0004,1430) value ("PATIENT", "STUDY",
"SERIES", "IMAGE" and the rest PS3.3 F.5 defines), or undefined when
the record carries none.
EmbeddedAttributeFinding
One value that was emptied because a Data Element was found inside it.
PS3.5 defines Value Length as the length of that element's own Value Field. A
sender that over-declares it produces a file whose reading is self-consistent
and whose next element has been absorbed into the previous one's value - and
Table E.1-1 is keyed by tag, so an absorbed (0010,0020) is not an attribute
any longer and no action fires on it. deidentify therefore refuses to keep a
value whose tail decodes as whole Data Elements it would have acted on
(PS3.15 §E.1 "all instances"; §E.3.5 is the standard's own precedent for
removing identifying information embedded inside a string attribute).
tag and vr are the carrier's own and are structural. hidden was not,
and it is bound now. Every tag in an embedded run is composed from four
bytes that were sitting inside the carrier's value - that is the whole
reason this type exists - and a run needs only ONE actionable attribute to be
reported, so through 0.0.13 the rest of the run was listed beside it.
Measured: a CS carrier over-declaring across a fabricated "SMIT" header
beside a genuine (0010,0020) reported hidden: ["4D535449", "00100020"],
and 4D535449 is "SMIT" in wire order.
An entry is now one of the 653 literal rows of PS3.15 Table E.1-1 that this
run's options left actionable. That is a membership bound rather than a
shape one - the posture this package already takes for a VR and for a Private
Creator - so what survives names a published table entry rather than a
document byte, the same trade rendering a VR makes with the 34. A
repeating-group mask hit is NOT in that set and is excluded: (50xx,xxxx)
Curve Data leaves the whole 16-bit element number free, so a mask match proves
a rule exists without making the membership finite. A graded pass caught a
draft of this filter that admitted it.
🛑 THAT IS NOT AN ALL-CLEAR OVER THIS TYPE. contextPath below is
unbound and unchanged, hidden is uncapped, and
DeidentifyReport names the report's other value-bearing fields. A
DeidentifyReport is still not safe to log whole.
Example
import { deidentify, parseDicom } from "@cosyte/dicom";
const { report } = deidentify(parseDicom(buf));
for (const e of report.embeddedAttributes) {
console.warn(`${e.tag} hid ${e.hidden.join(", ")} in its value`);
}
Properties
contextPath?
readonlyoptionalcontextPath?: readonlystring[]
Tag/index chain when the carrier is inside a sequence item; omitted at the root. Built by the same descent as DeidentifiedAttribute.contextPath and carrying the same caveat: each segment's tag is read off the wire, bound by nothing, so on a desynchronized read it can be four bytes of a value. Read that field's note before logging this one.
hidden
readonlyhidden: readonlystring[]
The tags found inside the carrier's value that this run acts on and that a published table names, in wire order. Not every tag in the run - see this type's own remarks.
🩺 IT MAY BE EMPTY, AND EMPTY DOES NOT MEAN "NOTHING WAS HIDDEN HERE".
A run whose only actionable members are private attributes, Curve Data or
Overlay elements reports a finding with no tags: the carrier was still
emptied, and the accompanying DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED
warning still counts the whole run. The presence of the finding is the fact;
this list is the part of it that can be named.
tag
readonlytag:string
The carrier - the element whose over-declared value held the others.
vr
readonlyvr:VR
The carrier's VR. Always one of the string VRs; binary VRs are not scanned.
FileMeta
The Part-10 File Meta Information group, projected as a typed view
over (0002,xxxx) elements parsed during the File Meta pre-pass.
Only transferSyntaxUID is required because it is the dispatch input
for the transfer-syntax parsers; everything else is optional
because real-world clinical files routinely omit one or more
Type-1 elements. validate() adds opinion-bearing checks
for those missing elements.
Example
import { parseDicom } from "@cosyte/dicom";
const ds = parseDicom(buf);
if (ds.fileMeta !== undefined) {
const ts = ds.fileMeta.transferSyntaxUID; // always present when fileMeta is defined
}
Properties
extraElements?
readonlyoptionalextraElements?: readonlyFileMetaRawElement[]
Any (0002,xxxx) elements the source carried that the typed fields above
do not model, preserved in tag order so a round-trip re-emits the File Meta
group byte-for-byte. Omitted when the group held only modeled elements.
🛑 The byte-for-byte round trip does NOT hold for deidentify() output
That promise is about parse then serialize, and it is scoped to it
deliberately rather than by omission. deidentify() rebuilds this group to
describe the de-identifying application instead of the source (PS3.15
§E.1.1: the File Meta Information "shall be replaced with a description of
the de-identifying application", because otherwise "identity information may
leak through unmodified File Meta Information ... includ[ing] information
regarding Application Entity Titles, Presentation Addresses, implementation
information, and private information"). So on the de-identify path this
array is dropped in full - not filtered, not sampled - and
sourceApplicationEntityTitle goes with it, while
implementationClassUID and implementationVersionName are replaced with
this library's own.
The elements that survive are the ones that identify the object rather
than its sender: fileMetaInformationVersion, mediaStorageSOPClassUID,
transferSyntaxUID, and mediaStorageSOPInstanceUID (remapped unless
RetainUIDs is active). What was dropped is counted on the report
deidentify() returns, so a caller learns the fidelity loss from the run
rather than by diffing the bytes.
A caller that needs the source group verbatim must read it off the parsed dataset; it is not recoverable from de-identified output, and it is not meant to be.
fileMetaInformationVersion?
readonlyoptionalfileMetaInformationVersion?:Buffer<ArrayBufferLike>
implementationClassUID?
readonlyoptionalimplementationClassUID?:string
implementationVersionName?
readonlyoptionalimplementationVersionName?:string
mediaStorageSOPClassUID?
readonlyoptionalmediaStorageSOPClassUID?:string
mediaStorageSOPInstanceUID?
readonlyoptionalmediaStorageSOPInstanceUID?:string
sourceApplicationEntityTitle?
readonlyoptionalsourceApplicationEntityTitle?:string
transferSyntaxUID
readonlytransferSyntaxUID:string
FileMetaDroppedElement
One non-modeled (0002,xxxx) File Meta element deidentify() dropped
rather than re-emitting into de-identified output.
Why the drop is unconditional
PS3.15 2026c §E.1.1 requires the File Meta Information of a de-identified
DICOM File to "be replaced with a description of the de-identifying
application", because otherwise "identity information may leak through
unmodified File Meta Information ... [t]his includes information regarding
Application Entity Titles, Presentation Addresses, implementation
information, and private information". A non-modeled (0002,xxxx) element is
by definition one this library cannot describe: (0002,0017) Sending AE
Title, (0002,0018) Receiving AE Title, (0002,0100) Private Information
Creator UID and (0002,0102) Private Information are all in that class, and
so is anything a vendor invented. The fail-safe direction is to drop what
cannot be described, so no Annex E Option and no parse quality changes the
outcome.
Capped record, complete count
The array is bounded per run (MAX_FILE_META_DROP_FINDINGS) because the
number of elements a crafted File Meta group can carry is chosen by the input.
The action is not bounded: every such element is dropped whether or not it
is listed, and DeidentifyReport.fileMetaElementsDroppedCount is the
complete total at any input size. An array exactly the cap's length means "at
least this many"; read the count, not the length.
What it carries
tag and vr come off the wire. The File Meta pre-pass stops at the first
element whose group is not 0002, so a tag reaching this record always has
0002 as its group, but its element number is two bytes the file supplied and
a File Meta element that over-declared its length can desynchronize the
pre-pass onto two bytes of somebody's value. Same standing exception as
DeidentifyReport.removedPrivateTags, by a narrower route and with two
bytes rather than four. byteLength is the length of the value that was
dropped, read off that same header. It is published because what was dropped
is the whole audit value of a record of a deliberate fidelity loss.
Example
import { deidentify, parseDicom, type FileMetaDroppedElement } from "@cosyte/dicom";
const { report } = deidentify(parseDicom(buf));
report.fileMetaElementsDropped.forEach((d: FileMetaDroppedElement) => {
console.warn(`${d.tag} ${d.vr}: ${String(d.byteLength)} bytes not re-emitted`);
});
Properties
byteLength
readonlybyteLength:number
Byte length of the value that is not in the de-identified output.
tag
readonlytag:string
The dropped element's (0002,xxxx) tag.
vr
readonlyvr:VR
The VR the source file wrote for it (the File Meta group is Explicit VR LE).
FileMetaRawElement
A non-modeled (0002,xxxx) File Meta element, preserved verbatim so the
serializer can re-emit an exotic File Meta group byte-for-byte.
The typed FileMeta fields cover the common Type-1/Type-3 elements;
anything else a source file carried - e.g. (0002,0017)/(0002,0018)
Sending/Receiving AE Title, (0002,0100) Private Information Creator UID,
(0002,0102) Private Information - is captured here as raw bytes (the
on-wire value, even-length per PS3.5 §6.2) rather than dropped. value is a
defensive copy, so the view never aliases the parsed input buffer.
🛑 THE ROUND TRIP IS PARSE-THEN-SERIALIZE ONLY. deidentify() DROPS EVERY
ELEMENT THIS TYPE DESCRIBES, and the exact examples above are why: a Sending
AE Title and a Private Information pair are the identity information PS3.15
§E.1.1 names. See FileMeta.extraElements for the whole carve-out.
Example
import { parseDicom } from "@cosyte/dicom";
const ds = parseDicom(buf);
// Anything the typed FileMeta fields do not model, in ascending tag order.
for (const raw of ds.fileMeta?.extraElements ?? []) {
raw.tag; // e.g. "00020017" Sending Application Entity Title
raw.vr; // the VR the source wrote (File Meta is always Explicit VR LE)
raw.value.length; // even, per PS3.5 2026c section 7.1.1
}
(0002,0016) Source Application Entity Title is deliberately NOT the example: it is one of the
typed fields, so it never reaches this array.
Properties
tag
readonlytag:string
8-char uppercase hex tag, e.g. "00020100".
value
readonlyvalue:Buffer
The raw on-wire value bytes (even-length), copied out of the input.
vr
readonlyvr:VR
The element's Value Representation as read under Explicit VR LE.
FrameFunctionalGroups
The five Enhanced multi-frame functional-group macros, resolved for a single frame Per-Frame-else-Shared (PS3.3 §C.7.6.16). Each macro is typed-absent when present in neither the per-frame nor the shared group (the three geometry macros are treated as required for an enhanced object - see ImageView.frame).
Example
import { parseDicom } from "@cosyte/dicom";
const f = parseDicom(enhancedBuf).image.frame(0);
f.planePosition?.imagePositionPatient; // this frame's [x,y,z]
f.pixelMeasures?.pixelSpacing; // this frame's [row,col] mm
Properties
frameVoiLut?
readonlyoptionalframeVoiLut?:object
windowCenter?
readonlyoptionalwindowCenter?: readonly (number|null)[]
windowWidth?
readonlyoptionalwindowWidth?: readonly (number|null)[]
index
readonlyindex:number
pixelMeasures?
readonlyoptionalpixelMeasures?:object
pixelSpacing?
readonlyoptionalpixelSpacing?: readonly (number|null)[]
sliceThickness?
readonlyoptionalsliceThickness?:number
spacingBetweenSlices?
readonlyoptionalspacingBetweenSlices?:number
pixelValueTransformation?
readonlyoptionalpixelValueTransformation?:object
rescaleIntercept?
readonlyoptionalrescaleIntercept?:number
rescaleSlope?
readonlyoptionalrescaleSlope?:number
rescaleType?
readonlyoptionalrescaleType?:string
planeOrientation?
readonlyoptionalplaneOrientation?:object
imageOrientationPatient?
readonlyoptionalimageOrientationPatient?: readonly (number|null)[]
planePosition?
readonlyoptionalplanePosition?:object
imagePositionPatient?
readonlyoptionalimagePositionPatient?: readonly (number|null)[]
Group0004Removal
One Data Element whose Group Number is 0004, removed from de-identified
output.
The rule, and the one object it does not apply to
PS3.15 2026c §E.1.1: "All Data Elements with a Group Number of 0004 shall be
removed from any SOP Instance or DICOM File other than a DICOMDIR File." The
rule is unconditional over the Annex E Options - it sits in the list §E.1.1
imposes on any de-identifier claim and no Option qualifies it - so no
retain argument brings a (0004,xxxx) element back.
The DICOMDIR carve-out is decided once per run from the source File Meta's
(0002,0002) Media Storage SOP Class UID, and only the value
1.2.840.10008.1.3.10 (Media Storage Directory Storage) selects it. An object
that declares no Media Storage SOP Class UID at all is therefore not a
DICOMDIR and the removal applies to it. When the carve-out does fire, this
array is empty and DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED says what
else the run did not do.
Every depth, and the removal decides nothing below it
The test runs on each element of each Data Set the run reaches, root and
Sequence Item alike, so a (0004,xxxx) planted inside an item is removed
there too. It is a test on one element's tag: it never stands in for a
decision about the Data Sets nested inside some other element, which is the
shape DICOM-PRIVATE-SQ-CARVE-OUT was opened for. Removing a (0004,xxxx)
that is itself a Sequence removes its items with it, exactly as a Table E.1-1
X action does.
Capped record, complete count
Bounded per run at MAX_GROUP_0004_FINDINGS, on its own counter so a flood of
one diagnostic class cannot spend another's budget. The removal itself is not
bounded, and DeidentifyReport.group0004RemovalCount is complete at any
input size.
What it carries
tag is composed from four bytes of the source and contextPath from four
more per segment - the standing exception
DeidentifyReport.removedPrivateTags and
UnauditableSequenceFinding already carry, by the same route. On a
well-formed file these are the DICOMDIR directory-structuring tags PS3.6
registers and carry nothing.
Example
import { deidentify, parseDicom, type Group0004Removal } from "@cosyte/dicom";
const { report } = deidentify(parseDicom(buf));
report.group0004Removals.forEach((r: Group0004Removal) => {
console.warn(`${r.tag} removed`, r.contextPath ?? "root");
});
Properties
applied
readonlyapplied:"removed"
The outcome, always "removed": no element bearing that tag is in the Data
Set that held it, in the returned dataset or in the serialized bytes.
contextPath?
readonlyoptionalcontextPath?: readonlystring[]
Tag/index chain when the element was inside a sequence item; omitted at the root. Built by the same descent as DeidentifiedAttribute.contextPath and carrying the same caveat: each segment's tag is read off the wire, bound by nothing, so on a desynchronized read it can be four bytes of a value. Read that field's note before logging this one.
tag
readonlytag:string
The removed element's tag. Its group is always 0004.
ImageView
Pixel-interpretation + geometry metadata - the "wrong pixels look fine" and "looks fine, measures wrong" classes. v1 does not decode pixels; this view surfaces exactly what a renderer needs so it does not have to guess.
Safety-critical omissions are intentional: rescaleSlope is absent (not
1) when the tag is absent; signed is absent (not a guess) unless
(0028,0103) was present; photometricInterpretation is absent (not
MONOCHROME2) when absent; the three pixel-spacing fields are distinct.
When modalityLutSequence / voiLutSequence are present they are
authoritative over the linear rescale* / window* pairs.
Example
import { parseDicom } from "@cosyte/dicom";
const img = parseDicom(buf).image;
img.rescaleSlope; // undefined ⇒ caller MUST NOT assume 1
img.signed; // undefined ⇒ signedness unknown, never guess
img.pixelSpacing; // patient-plane mm - distinct from imagerPixelSpacing
Properties
bitsAllocated?
readonlyoptionalbitsAllocated?:number
bitsStored?
readonlyoptionalbitsStored?:number
columns?
readonlyoptionalcolumns?:number
frameOfReferenceUid?
readonlyoptionalframeOfReferenceUid?:string
highBit?
readonlyoptionalhighBit?:number
imageOrientationPatient?
readonlyoptionalimageOrientationPatient?: readonly (number|null)[]
imagePositionPatient?
readonlyoptionalimagePositionPatient?: readonly (number|null)[]
imagerPixelSpacing?
readonlyoptionalimagerPixelSpacing?: readonly (number|null)[]
isEnhancedMultiFrame
readonlyisEnhancedMultiFrame:boolean
true when this object carries Per-Frame/Shared Functional Groups.
modalityLutSequence?
readonlyoptionalmodalityLutSequence?: readonlyItem[]
nominalScannedPixelSpacing?
readonlyoptionalnominalScannedPixelSpacing?: readonly (number|null)[]
numberOfFrames?
readonlyoptionalnumberOfFrames?:number
photometricInterpretation?
readonlyoptionalphotometricInterpretation?:string
pixelRepresentation?
readonlyoptionalpixelRepresentation?:number
Raw (0028,0103) value: 0 = unsigned, 1 = signed. Absent ⇒ unknown.
pixelSpacing?
readonlyoptionalpixelSpacing?: readonly (number|null)[]
planarConfiguration?
readonlyoptionalplanarConfiguration?:number
realWorldValueMaps?
readonlyoptionalrealWorldValueMaps?: readonlyRealWorldValueMap[]
rescaleIntercept?
readonlyoptionalrescaleIntercept?:number
rescaleSlope?
readonlyoptionalrescaleSlope?:number
rescaleType?
readonlyoptionalrescaleType?:string
rows?
readonlyoptionalrows?:number
samplesPerPixel?
readonlyoptionalsamplesPerPixel?:number
signed?
readonlyoptionalsigned?:boolean
true/false only when (0028,0103) was 1/0; absent ⇒ never guessed.
sliceThickness?
readonlyoptionalsliceThickness?:number
sopInstanceUid?
readonlyoptionalsopInstanceUid?:string
spacingBetweenSlices?
readonlyoptionalspacingBetweenSlices?:number
units?
readonlyoptionalunits?:string
voiLutSequence?
readonlyoptionalvoiLutSequence?: readonlyItem[]
windowCenter?
readonlyoptionalwindowCenter?: readonly (number|null)[]
windowWidth?
readonlyoptionalwindowWidth?: readonly (number|null)[]
Methods
frame()
frame(
index):FrameFunctionalGroups
Resolve the functional-group macros for frame index Per-Frame-else-Shared.
Throws DicomValueError FRAME_INDEX_OUT_OF_RANGE for an index
outside [0, numberOfFrames) and MISSING_REQUIRED_FUNCTIONAL_GROUP
when an enhanced object lacks a required geometry macro in both groups.
Parameters
index
number
Returns
OtherPatientId
One entry of Other Patient IDs Sequence (0010,1002) - a {id, issuer, typeCode} triple (PS3.3 §10.15, the DICOM analogue of an HL7 v2 CX
repetition). Surfaced so a caller never matches on a bare (0010,0020).
Example
import type { OtherPatientId } from "@cosyte/dicom";
const o: OtherPatientId = { id: "MRN-42", issuer: "HOSP_A", typeCode: "TEXT" };
Properties
id?
readonlyoptionalid?:string
issuer?
readonlyoptionalissuer?:string
typeCode?
readonlyoptionaltypeCode?:string
ParseOptions
Options accepted by parseDicom.
With exactOptionalPropertyTypes: true, callers omit unset keys
rather than passing undefined for any field below.
Example
import { parseDicom } from "@cosyte/dicom";
const ds = parseDicom(buf, {
strict: false,
stripPreamble: "tolerate",
copyValues: false,
onWarning: (w) => console.warn(w.code),
});
Properties
copyValues?
readonlyoptionalcopyValues?:boolean
When true, every Element.rawBytes is Buffer.from(slice) - copying
each value out so the source buffer can be released. When false (the
default), Element.rawBytes is Buffer.subarray(slice) - a zero-copy
view that pins the source ArrayBuffer until every Element is GC'd.
Omit to use the default.
onWarning?
readonlyoptionalonWarning?:OnWarningCallback
Synchronous callback invoked once per Tier-2 warning, after the warning
has been pushed to Dataset.warnings. Throwing handlers are silently
swallowed, for parser-state safety.
Omit to skip the callback entirely.
profile?
readonlyoptionalprofile?:Profile
Source/vendor tolerance preset. Applies the profile's
escalations / suppressions to warning emission and its
privateDictionary to Implicit-VR resolution of private data elements.
A profile only tightens or annotates - it never makes the default
lenient parse throw outside the four Tier-3 fatals, and a private
creator the profile does not recognize degrades to generic UN handling
plus a DICOM_PRIVATE_CREATOR_UNKNOWN warning, never a wrong decode.
Omit (do not pass undefined) for the unprofiled default behaviour.
strict?
readonlyoptionalstrict?:boolean
When true, every Tier-2 warning is escalated to a thrown
DicomParseError carrying the warning code. Default false.
🛑 THE ESCALATED DIAGNOSTIC CARRIES SOURCE BYTES THAT THE WARNING DOES
NOT. A DicomParseWarning.message is a frozen registry string with only
structural tokens filled in: {tag} renders only a tag PS3.6's element
registry carries a literal row for, {vr} only one of the 34 VRs PS3.5
section 6.2 defines, and a raw length or byte value a header carries is
bound out of the factory signature rather than rendered. That now holds on
the deidentify() codes too:
DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE and
DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE rendered Element.rawBytes.length
through 0.0.14, which equals the declared Value Length and was reachable
from a fabricated header, and both slots are gone. So is
DICOM_ITEM_CROSSES_SEQUENCE_END's remaining-bytes count, because a raw
number shifted by a constant the reader can compute is that raw number: it
was the enclosing sequence's declared Value Length less the bytes of that
sequence already consumed. The exceptions are named in one place that is
not a record of a past change, and are deliberately not restated here -
the WARNING_MESSAGES docblock in ./warnings.ts, which this JSDoc used to
carry a copy of.
This is a statement about w.message and about nothing else: the two
byte counts still exist on report.undefinedVrElements[].byteLength and
report.unauditableSequences[].byteLength, model fields on a type whose own
docs say it is not a value-free surface. No
safe-to-log verdict is stated here - that sentence was corrected twice and
is deleted rather than tried a third time; the mechanism is above and the
treatment is in the package's troubleshooting docs. The DicomParseError
this option raises in its place is a different and larger surface: it also
carries snippet,
16 raw bytes, unredacted, read at the warning's own byteOffset.
A message-only PHI review of the lenient path therefore does not transfer to
the strict one. Log err.code, err.byteOffset, err.offsetFrame and
err.message; treat err.snippet as PHI.
The snippet is cut in the SAME FRAME the byteOffset is counted in, so
it is the bytes at the offset the diagnostic names: file-absolute at the
root, relative to the enclosing slice inside a defined-length Sequence or
Item, and into the inflated stream under Deflated Explicit VR LE. It was not
always: until DICOM-FATAL-MESSAGE-REGISTRY the offset moved with the frame
while the cut was always taken from the whole file, so inside a
defined-length Item the 16 bytes were an unrelated element's - a
diagnostic disclosing data from a part of the document the reader was never
asked about. That is closed. What is NOT closed, and never was a defect:
the bytes are still raw source bytes. Reading them as safe because the
message beside them is registry-bound is the mistake this whole paragraph
exists to prevent.
byteOffset NOW CARRIES A FRAME-OF-REFERENCE CONTRACT, AND IT IS A NAME
AND NOT AN ORIGIN. err.offsetFrame says which of three coordinate
systems the number is counted in (OFFSET_FRAMES), so a consumer can tell a
root offset from an Item-relative one instead of guessing. A nested offset
is still not a key you can look up against the root, and it is not made
into one here: where a slice begins is deliberately unpublished, because the
distance between two frames is a declared Value Length off the wire. The
escalated warning's own position is unchanged and still carries no frame
beyond deflated - see DicomPosition.
Omit (do not pass undefined) to use the default.
stripPreamble?
readonlyoptionalstripPreamble?:"tolerate"|"require"
Preamble policy:
"tolerate"(default): attempt to start at offset 0 ifDICMmagic is missing at offset 128; emitDICOM_MISSING_PREAMBLE."require": throwDicomParseError(NOT_DICOM_PART_10)when noDICMmagic is present.
Omit to use the default.
PatientView
Patient & study identity (the wrong-patient failure class).
id is not globally unique; correct cross-system matching needs the
{id, issuerOfId, issuerQualifiers} tuple plus otherIds. name keeps
its full PN component structure, never flattened to a string.
Example
import { parseDicom } from "@cosyte/dicom";
const p = parseDicom(buf).patient;
p.id; // "MRN-42" - meaningless without the issuer
p.issuerOfId; // "HOSP_A"
p.name?.alphabetic.familyName; // "Doe"
Properties
birthDate?
readonlyoptionalbirthDate?:DicomDate
id?
readonlyoptionalid?:string
issuerOfId?
readonlyoptionalissuerOfId?:string
issuerQualifiers?
readonlyoptionalissuerQualifiers?: readonlyItem[]
name?
readonlyoptionalname?:PersonName
otherIds
readonlyotherIds: readonlyOtherPatientId[]
sex?
readonlyoptionalsex?:string
PersonName
A decoded PN value - up to three component groups separated on-wire by
= (PS3.5 §6.2.1.1). alphabetic is always present; ideographic and
phonetic are present only when the value supplied them.
Example
import type { PersonName } from "@cosyte/dicom";
// "Yamada^Tarou=山田^太郎=やまだ^たろう"
declare const pn: PersonName;
pn.alphabetic.familyName; // "Yamada"
pn.ideographic?.familyName; // "山田"
Properties
alphabetic
readonlyalphabetic:PersonNameGroup
ideographic?
readonlyoptionalideographic?:PersonNameGroup
phonetic?
readonlyoptionalphonetic?:PersonNameGroup
PersonNameGroup
One of the three component groups of a PN value (PS3.5 §6.2.1.1):
alphabetic, ideographic, or phonetic. Each holds the five ^-delimited
components; missing components are the empty string (never undefined).
Example
import type { PersonNameGroup } from "@cosyte/dicom";
const g: PersonNameGroup = {
familyName: "Doe",
givenName: "Jane",
middleName: "",
namePrefix: "",
nameSuffix: "",
};
Properties
familyName
readonlyfamilyName:string
givenName
readonlygivenName:string
middleName
readonlymiddleName:string
namePrefix
readonlynamePrefix:string
nameSuffix
readonlynameSuffix:string
PixelDataFragments
The Basic Offset Table and the fragments of one encapsulated Pixel Data element, each as the raw bytes of its Item Value.
Every Buffer here is a view over the element's own Element.rawBytes, with
no Item header and no Sequence Delimitation Item in it, so it follows that
element's retention: a zero-copy view over your input by default, and a copy
that outlives it under parseDicom(bytes, { copyValues: true }).
Example
import { parseDicom, readPixelDataFragments } from "@cosyte/dicom";
const pixels = readPixelDataFragments(parseDicom(buf));
if (pixels !== undefined) {
pixels.basicOffsetTable?.length; // 0 when the table is empty
pixels.fragments.length; // how many fragment Items the file carries
}
Properties
basicOffsetTable
readonlybasicOffsetTable:Buffer<ArrayBufferLike> |undefined
The Basic Offset Table's Item Value, uninterpreted. Zero bytes when the
table is empty, which PS3.5 2026c section A.4 permits and decoders must
accept. undefined only when the Item stream holds no Item at all, which
A.4 does not permit; nothing is invented in its place.
fragments
readonlyfragments: readonlyBuffer<ArrayBufferLike>[]
The Item Value of every Item after the Basic Offset Table, in file order. Each is returned whole: its content is never read, so bytes inside a fragment that happen to look like a delimiter are fragment bytes (A.4: a decoder "may not scan for a Sequence Delimitation Item").
PrivateTagDefinition
One private-data attribute definition supplied by a Profile's
private-dictionary overlay. The vr resolves the Implicit-VR of a private
data element whose on-wire encoding carries no VR; keyword / name carry
the vendor-documented identity for tooling and docs.
Example
import type { PrivateTagDefinition } from "@cosyte/dicom";
const def: PrivateTagDefinition = {
vr: "OB",
keyword: "CSAImageHeaderInfo",
name: "CSA Image Header Info",
};
Properties
keyword
readonlykeyword:string
name
readonlyname:string
vr
readonlyvr:VR
Profile
A source/vendor tolerance preset. A Profile bundles three
things that only ever tighten or annotate a parse - never loosen it
past the Postel's-Law default:
escalations- Tier-2 warning codes promoted to a thrownDicomParseError(a stricter posture for known-unsafe deviations).suppressions- Tier-2 warning codes silenced because they are a documented, benign quirk of the named source (annotation, not loss).privateDictionary- a private-creator-keyed overlay resolving the Implicit-VR of vendor private data elements via the file's live private-creator string (never a hard-coded block number).
Build one with defineProfile(); never hand-author the frozen shape.
Profiles are immutable and composable via extends.
Example
import { parseDicom, profiles } from "@cosyte/dicom";
const ds = parseDicom(buf, { profile: profiles.siemens });
console.log(ds.fileMeta?.transferSyntaxUID);
Properties
describe?
readonlyoptionaldescribe?: () =>string
Render a human-readable, deterministic one-line summary of the profile.
Returns
string
description?
readonlyoptionaldescription?:string
escalations
readonlyescalations:ReadonlySet<WarningCode>
lineage
readonlylineage: readonlystring[]
name
readonlyname:string
privateDictionary
readonlyprivateDictionary:ReadonlyMap<string,ReadonlyMap<string,PrivateTagDefinition>>
Creator string → canonical private-tag key ("GGGGxxEE", e.g.
"0029xx10") → definition. The xx placeholder stands for the
file-assigned private block byte, mirroring the published DICOM
private-dictionary notation; resolution is therefore by creator string,
never by a fixed block number.
suppressions
readonlysuppressions:ReadonlySet<WarningCode>
RealWorldValueMap
A Real World Value Mapping - slope/intercept bound atomically to
its UCUM measurement-units code, so a number is never detached from its
units. From Real World Value Mapping Sequence (0040,9096).
Example
import type { RealWorldValueMap } from "@cosyte/dicom";
const m: RealWorldValueMap = { slope: 1, intercept: 0, unitsCode: { codeValue: "[hnsf'U]" } };
Properties
intercept?
readonlyoptionalintercept?:number
slope?
readonlyoptionalslope?:number
unitsCode?
readonlyoptionalunitsCode?:CodedConcept
SeriesView
Series-level identity & co-registration. Images sharing a
frameOfReferenceUid are spatially co-registered.
Example
import { parseDicom } from "@cosyte/dicom";
const s = parseDicom(buf).series;
s.modality; // "CT"
s.frameOfReferenceUid; // shared ⇒ co-registered
Properties
description?
readonlyoptionaldescription?:string
frameOfReferenceUid?
readonlyoptionalframeOfReferenceUid?:string
instanceUid?
readonlyoptionalinstanceUid?:string
modality?
readonlyoptionalmodality?:string
number?
readonlyoptionalnumber?:number
StudyView
Study-level identity. instanceUid is the cross-system study key;
accessionNumber ties the study to the order.
Example
import { parseDicom } from "@cosyte/dicom";
const s = parseDicom(buf).study;
s.instanceUid; // "1.2.840.113619..."
s.accessionNumber; // "ACC123"
Properties
accessionNumber?
readonlyoptionalaccessionNumber?:string
date?
readonlyoptionaldate?:DicomDate
description?
readonlyoptionaldescription?:string
id?
readonlyoptionalid?:string
instanceUid?
readonlyoptionalinstanceUid?:string
time?
readonlyoptionaltime?:DicomTime
ToDateOptions
The one option toDate takes, and the only key it carries.
assumeOffsetMinutes is the caller's declaration of the zone a value that
carries none was written in. An explicit 0 means "read this naive value as
UTC", which is a decision this library will not make on a caller's behalf.
Example
import { parseDate, toDate } from "@cosyte/dicom";
import type { ToDateOptions } from "@cosyte/dicom";
const options: ToDateOptions = { assumeOffsetMinutes: -300 };
toDate(parseDate("20240115").value, options)?.toISOString(); // "2024-01-15T05:00:00.000Z"
Properties
assumeOffsetMinutes?
readonlyoptionalassumeOffsetMinutes?:number
Minutes east of UTC to read a value that states no offset in.
UidRemapper
A UID remapper: a stable map(src) plus the backing cache it fills.
Example
import { makeUidRemapper, type UidRemapper } from "@cosyte/dicom";
const remap: UidRemapper = makeUidRemapper();
const replaced = remap.map("1.2.840.10008.5.1.4.1.1.2");
remap.cache.get("1.2.840.10008.5.1.4.1.1.2") === replaced; // true
Properties
cache
readonlycache:Map<string,string>
The source→replacement cache, exposed for reporting / reuse.
map
readonlymap: (sourceUid) =>string
Map one source UID to its deterministic replacement (cached).
Parameters
sourceUid
string
Returns
string
UnauditableSequenceFinding
One carrier this run could not reach the Data Sets inside, and emptied for it. UnauditableSequenceFinding.applied names that outcome and is the field to read first; it is the only outcome this finding has.
Two producers, and neither ships bytes. The ordinary one is an
SQ element whose items the parser never materialized. The second is a
private data element retained under RetainSafePrivate whose Profile entry
declares it SQ while the parse tree says otherwise - UN under Implicit VR
LE when the profile was passed to deidentify() but not to parseDicom, or
whatever binary VR the sender wrote under Explicit VR, which wins in the
parser. The profile is the authority that retained the element, and it has
said the value is a Sequence of Items; with no items on the tree, the §E.1.1
obligation below falls on the carrier just the same. Such an element keeps its
parsed VR in the output and is emptied rather than re-typed to SQ
(DICOM-PRIVATE-SQ-PARSE-VR).
🛑 THERE WAS A THIRD PRODUCER AND IT IS RETIRED: applied: "kept" IS GONE
FROM THIS TYPE AND FROM EVERY RUN. Through 0.0.19 a private data element
the profile vouched for whose value this run never enumerated was retained
verbatim and named here with applied: "kept" - the package reporting that
it had shipped a value it did not read. Such an instance is now removed
instead, and its record moved to
DeidentifyReport.unenumerablePrivateRemovals, which is a different
surface with a different guarantee: uncapped, complete, and stating a reason.
This array never carries a retained private value again, so an entry here
always means content is not in your output. That is an audit-contract
change for anyone who switched on applied; see
UnenumerablePrivateRemoval.
PS3.5 2026c §7.5.1 "Item Encoding Rules" states that "Each Item Value shall
contain a DICOM Data Set composed of Data Elements", so an SQ element's
value is never opaque bytes - it is Data Elements this run is obliged to
reach. PS3.15 2026c §E.1.1 "De-identifier" states that obligation directly:
an implementation claiming the Basic Application Level Confidentiality
Profile "shall protect or retain all instances of the Attributes listed in
[Table E.1-1], whether contained in the top level Data Set or embedded in an
Item of a Sequence of Items". When the item stream cannot be enumerated the
obligation cannot be discharged element by element, so it falls on the
enclosing attribute - the escalation §E.1.1 itself uses for a SOP Instance
UID inside a Sequence, where "the enclosing Attribute in the top-level Data
Set must be encrypted in its entirety". (That sentence is written about the
encrypt-and-replace mechanism for SOP Instance UIDs, not about Table E.1-1
generally; it is cited here as the standard's own precedent for escalating to
the carrier, not as a rule about this case.)
Both fields are structural: tag is the carrier's and byteLength is the
recorded span of the value that was dropped. No decoded value appears
here, and that is not the same as "safe to log". On the second producer
tag can be four bytes of another element's value: a length under-declared
upstream resynchronizes the reader mid-value, and if the bytes it lands on
spell a private block this caller's profile declares SQ, followed by a VR
that is one of the 34, the fabricated header is what you get here. The
package's answer to that class is normally report.undefinedVrElements,
which names a byte offset and no tag, and it still answers the case where
the fabricated VR is outside the 34 - but it cannot answer this one, because
a fabricated OB header and a genuine one are byte-identical. So this shares
the standing exception report.removedPrivateTags and uidMap already have:
a DeidentifyReport is not a value-free surface. Treat it as document
content, at the sensitivity of the file it came from.
For the first producer the parser announces the underlying refusal on
Dataset.warnings: DICOM_SQ_NOT_DESCENDED for a defined-length Implicit VR
LE value whose dictionary-resolved SQ was not a valid item stream. Do not
generalise that to the second. There the file may be entirely conformant -
an honest defined-length OB carrier raises nothing at all - so this report
field, not Dataset.warnings, is where that drop is visible.
Example
import { deidentify, parseDicom } from "@cosyte/dicom";
const { report } = deidentify(parseDicom(buf));
for (const s of report.unauditableSequences) {
console.warn(`${s.tag}: ${String(s.byteLength)} bytes dropped, item stream unreadable`);
}
Properties
applied
readonlyapplied:"emptied"
What happened to that carrier's value: "emptied", always. The value is not
in the de-identified output.
🛑 "kept" WAS A MEMBER OF THIS UNION AND IS NOT ANY MORE. It meant "the
value IS in the output, byte for byte, unexamined", and a report field that
can say so is exactly what this package no longer does: that instance is
removed now and recorded on
DeidentifyReport.unenumerablePrivateRemovals. The field itself is
kept, so applied === "emptied" keeps compiling and keeps meaning what it
always meant; a comparison against "kept" does not compile any more, which
is the intended way to find out.
byteLength
readonlybyteLength:number
Byte length of the carrier's value field that was dropped. Structural, never a decoded value.
contextPath?
readonlyoptionalcontextPath?: readonlystring[]
Tag/index chain when the carrier is inside a sequence item; omitted at the root. Built by the same descent as DeidentifiedAttribute.contextPath and carrying the same caveat: each segment's tag is read off the wire, bound by nothing, so on a desynchronized read it can be four bytes of a value. Read that field's note before logging this one.
tag
readonlytag:string
The carrier element.
UndefinedVrFinding
One element that was emptied because its on-wire VR is not one of the 34 PS3.5 section 6.2 defines, so nothing this library did to its bytes counts as decoding a Value Field.
Why such an element exists at all
Under an Explicit VR Transfer Syntax the VR is two bytes the sender wrote, and this parser trusts them (Postel's Law on the read path). The routine way two arbitrary bytes end up in a VR field is an under-declared Value Length upstream: the reader finishes the short value, and the leftover bytes of the value that was actually encoded are read as the next Data Element header. Tag, VR and length are then all fragments of somebody's value, and the element that genuinely followed is consumed as this fabricated element's "value".
Measured on scripts/measure-sq-bound-grid.ts: a carrier under-declaring by 6
produces (4156,554C) with the VR bytes "E ", whose value holds the source
(0010,0020) Patient ID in full. It reaches string carriers exactly as it
reaches binary ones, because the carrier's own VR is not what decides it.
Why emptying, and why it is not a guess
PS3.5 2026c section 6.2 requires every VR not yet defined to use the long-form Data Element Structure - "with reserved bytes after the VR and a 32-bit unsigned integer VL" - so an unrecognized VR read short-form, which is what this parser does, is by the standard's own structure rule not a reading of a Value Field. There is nothing to prove about the content: the test is a membership check against the closed 34-VR set on a field the parser already recorded, so there is no scan, no per-offset loop, and no cost that follows an attacker-chosen value length.
PS3.15 2026c section E.1.1 obliges an implementation claiming the Basic Application Level Confidentiality Profile to "protect or retain all instances of the Attributes listed in [Table E.1-1]". Those instances cannot be reached inside bytes that were never a value, so the obligation falls on the carrier.
UN is not this. UN is one of the 34, so an ordinary unknown-VR element
- the Implicit VR fallback for a tag this build's dictionary does not publish,
and the CP-246 shape - never reaches here. That is the line the sibling
SQ-with-no-items rule could not draw.
Why this finding names no tag, when every sibling finding does
Because the tag may be content, and nothing here can tell. The paragraph
above is the whole argument: when an under-declare desynchronized the reader,
the four tag bytes and the two VR bytes were read out of the middle of some
element's Value Field, so reporting the "tag" would republish four bytes of
the document. An unrecognized VR written honestly, at a correct length, raises
this same code and has an ordinary tag - and the two are indistinguishable
here, so the tag is withheld on both routes rather than on a guess. Measured on a synthetic ST carrier holding
"MR BRAIN SMITHSON", the fabricated tag is 48544F53 - four bytes of the
surname. EmbeddedAttributeFinding and
UnauditableSequenceFinding may carry a tag because theirs came from a
header the sender really wrote; this one may not, and the asymmetry is the
finding rather than an inconsistency.
byteOffset locates the element instead - a position this parser counted.
🩺 THAT WITHHOLDING IS NOT WHOLE, AND THIS PARAGRAPH USED TO CLAIM IT WAS.
It said "nothing here renders a document byte ... and the structural
contextPath. Safe to log." contextPath is not structural: its segments are
tags read off the wire by the same descent, so the header this type refuses to
name by tag can be named by the contextPath of a finding one level down.
See DeidentifiedAttribute.contextPath for the measurement. byteOffset
and byteLength are unaffected and the reasoning above them still stands.
Example
import { deidentify, parseDicom } from "@cosyte/dicom";
const { report } = deidentify(parseDicom(buf));
for (const u of report.undefinedVrElements) {
console.warn(`offset ${String(u.byteOffset)}: ${String(u.byteLength)} bytes dropped`);
}
Properties
byteLength
readonlybyteLength:number
Byte length of the value field that was dropped.
An input-derived count, and on this finding specifically that is not a
formality. For the sibling findings the length came from a header the
sender wrote; here the four length bytes can themselves be value bytes, like
the tag bytes. What is published is the number they decode to, and one
readUInt32LE puts the bytes back: a fabricated header reading "SO\0\0"
publishes 20307, two letters of a surname. Do not describe this field as
"structural, never a value" the way its siblings are described.
🩺 IT IS NO LONGER "the same footing as every {n} in the warning
registry", AND THAT SENTENCE WAS THE ONE THIS FIELD SHIPPED WITH.
DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE rendered exactly this number and
does not any more - it is bound out of undefinedVrNotAuditable's
signature. So this field is now a model field, on the same standing
exception as removedPrivateTags, unauditableSequences[].tag, uidMap
and contextPath: narrowing it is a product call, because a bound empties
it on every well-formed file, where it is exactly the audit number the field
exists to carry. Deliberately unchanged; see this type's own summary.
byteOffset
readonlybyteOffset:number
Byte offset of the emptied element's header. This is how the element is
identified, and there is deliberately no tag field - see the note above:
a fabricated header's tag bytes are part of some element's value.
contextPath?
readonlyoptionalcontextPath?: readonlystring[]
Tag/index chain when the carrier is inside a sequence item; omitted at the root. Built by the same descent as DeidentifiedAttribute.contextPath and carrying the same caveat: each segment's tag is read off the wire, bound by nothing, so on a desynchronized read it can be four bytes of a value. Read that field's note before logging this one.
UnenumerablePrivateRemoval
One private attribute removed because this run did not enumerate its value - the record that discharges the audit half of the fail-safe, and the one report surface that is complete at any input size.
What "enumerated" means here, and what it deliberately does not
A run has enumerated a private value when it walked it as DICOM Data
Elements and put each of them through the Annex E action table (a private
SQ the parser materialized items for), when the whole value was matched
as a member of a closed table the caller supplied (a Private Creator
(gggg,00EE) whose decoded value is in the profile's private dictionary), or
when the value is zero-length and so encodes no Data Set. Nothing else.
🛑 DECODING A VALUE UNDER THE VR THE PROFILE RESOLVED FOR IT IS NOT
ENUMERATION, AND NEITHER IS THE EMBEDDED-ATTRIBUTE SCANNER'S SILENCE. A
decoded LO, ST, OB, OW or UN value is a byte run that can carry a
Data Set written in a transfer syntax this run never tested for; two cells of
the matrix in test/integration/deident-private-reservation.test.ts are
perfectly scannable Implicit VR LE string carriers whose values the scanner
DID read and found nothing in, and which carried a nested (0010,0010)
anyway, because the nested tiles are Explicit VR and the file is Implicit. So
the predicate is what the run did with the value, never the VR and never
the scanner's reach.
The cost, stated at its real size
With RetainSafePrivate plus a Profile, three classes of private value
reach the output on something this run enumerated: an instance the run walked
as Data Elements, a Private Creator the profile's dictionary vouches for, and
a zero-length value. An ordinary vendor scalar under an ordinary string VR
that the file's own declaration does not name safe is removed, because
this package enumerates nothing inside a retained private value that is not
one of those three. That is over-redaction traded for a closed identity leak:
PS3.15 2026c §E.3.10 retains Private Attributes "known by the de-identifier to
be safe from identity leakage" and sends "all other Private Attributes" to
removal or to the (0008,0307) element-specific action, which this library
does not implement - and a value nothing enumerated is not known to be safe
however ordinary it looks. Through 0.0.19 such a value was kept and
disclosed instead; the disclosure said outright that it was not a fix.
🩺 The fourth class, which this record never names
A private value reaches the output on one more route, and it is the one that
rests on no enumeration at all: the file's own Private Data Element
Characteristics Sequence (0008,0300), which §E.3.10 names first among the ways
an Attribute is known safe and which needs no Profile. A value in a block
whose Block Identifying Information Status (0008,0303) reads SAFE, or listed
in a MIXED block's Nonidentifying Private Elements (0008,0304), is retained
unexamined on the sender's written assertion, so it is never removed and
never appears here. That is the opposite trade from the one above, made in the
open: the caller who does not trust the sender leaves RetainSafePrivate off,
which is the one mitigation §E.3.10 offers.
Per INSTANCE, never per tag
An entry names one occurrence: a tag together with the Data Set it lived
in, which contextPath identifies (absent means the root Data Set). Private
blocks are reserved per Data Set (PS3.5 §7.8.1), so the same private tag can
occur several times in one object with different enumerability, and removing
one occurrence never removes a sibling one.
Uncapped, and that is a decision rather than an oversight
Every consumer-controlled diagnostic in this package is capped, because a finding emitted per element is amplified by an element count the input chooses. This is not a diagnostic: it is the record of an action, on the same footing as DeidentifyReport.removedPrivateTags, which is uncapped for the same reason. A caller's whole guarantee is that they can separate the unenumerable removals from the Annex E ones for every removal, so a cap here would silently take the guarantee away exactly on the files that need it most. The matching warnings stay bounded; this array does not.
What it carries, and what that is worth logging
tag is composed from four bytes of the source. On a well-formed file it is
the sender's own private tag number and carries nothing, but on a file whose
Value Lengths disagree with its bytes those four bytes can be document
content - the standing exception DeidentifyReport.removedPrivateTags
and UnauditableSequenceFinding already carry, by the same route, and
contextPath is unbound in the same way. It is published anyway, because
which attribute was removed and where is the whole audit value of the
record. Treat this array as PHI when the source is untrusted.
Example
import { deidentify, parseDicom, type UnenumerablePrivateRemoval } from "@cosyte/dicom";
const { report } = deidentify(parseDicom(buf), { retain: ["RetainSafePrivate"], profile });
report.unenumerablePrivateRemovals.forEach((r: UnenumerablePrivateRemoval) => {
console.warn(`${r.tag} ${r.applied} (${r.reason})`, r.contextPath ?? "root");
});
Properties
applied
readonlyapplied:"removed"
The outcome, always "removed": the Data Set that held this instance
carries no element bearing that tag, in the returned dataset and in the
serialized bytes. Distinct from emptied, where an element with that tag
is present carrying a zero-length value.
contextPath?
readonlyoptionalcontextPath?: readonlystring[]
Tag/index chain naming the Data Set this instance lived in; omitted at the root, which is what identifies the removal as per-instance rather than per-tag. Built by the same descent as DeidentifiedAttribute.contextPath and carrying the same caveat: each segment's tag is read off the wire, bound by nothing, so on a desynchronized read it can be four bytes of a value. Read that field's note before logging this one.
reason
readonlyreason:"unenumerable"
Why, always "unenumerable": this run did not enumerate the value, so
PS3.15 §E.3.10's "known ... to be safe" was never established for it. This
is what separates an entry here from an attribute the Annex E action table
removed (report.attributes, applied: "removed") and from one whose value
was emptied.
tag
readonlytag:string
The removed instance's tag.
UnregisteredElementRemoval
One non-private Data Element removed because this build's PS3.6 2026d registry does not carry its tag and PS3.15 2026d Table E.1-1 does not list it.
The rule
A Table E.1-1 miss used to mean "keep": an attribute the table does not list
went into de-identified output verbatim with a report that said nothing about
it. That is right for an attribute PS3.6 registers, which the Profile has
judged, and wrong for one it does not: the notes to PS3.15 2026d Table E.1-1
name "new Standard Attributes" among the places identifying information may
be, and say removing only the known risks "may fail when the Standard is
extended, or when a vendor adds unanticipated Standard Attributes". So an
even-group tag with no registry row and no Table E.1-1 row is removed, at
every depth deidentify() walks, whatever its VR (UN, an on-wire VR outside
the 34 PS3.5 section 6.2 defines, and SQ included; a Sequence goes whole and
nothing inside it is walked or recorded).
"Registered" means a literal PS3.6 row, or a masked row the tag matches, with
the 50xx / 60xx groups bounded by PS3.5 2026c section 7.6 (even groups
5000-501E and 6000-601E). Private tags, group 0004, group 0002,
group lengths (gggg,0000) and every tag Table E.1-1 lists are not decided by
this rule.
What it costs
A conformant attribute from a PS3.6 edition newer than this build's pin is removed too, because nothing separates it from an invented one. That is over-redaction and it is deliberate; a caller who needs such an attribute back has no switch for it in this release.
What it carries, and what it deliberately does not
No tag and no VR. The trigger is "no registry row carries this tag", and
four bytes an under-declared length upstream made the reader take as a header
satisfy it by construction: the fixture in
test/integration/deident-undefined-vr.test.ts fabricates (4854,4F53),
"THSO" in wire order, four letters of a surname. Publishing the tag would
republish them, so the element is identified by byteOffset, a position this
parser counted, exactly as UndefinedVrFinding does.
contextPath names the Sequences this run descended to reach the
element, never the removed element itself, and carries the caveat every
finding's contextPath carries: see DeidentifiedAttribute.contextPath.
Capped record, complete count
Bounded per run at MAX_UNREGISTERED_ELEMENT_FINDINGS, on its own counter so
a flood of one diagnostic class cannot spend another's budget. The removal
itself is not bounded, and
DeidentifyReport.unregisteredElementRemovalCount is complete at any
input size.
Example
import { deidentify, parseDicom, type UnregisteredElementRemoval } from "@cosyte/dicom";
const { report } = deidentify(parseDicom(buf));
report.unregisteredElementRemovals.forEach((r: UnregisteredElementRemoval) => {
console.warn(`removed at offset ${String(r.byteOffset)}`, r.contextPath ?? "root");
});
Properties
byteOffset
readonlybyteOffset:number
Byte offset of the removed element's header, as the parser recorded it on
Element.byteOffset. This is how the element is identified, and there is
deliberately no tag field: see the note above.
contextPath?
readonlyoptionalcontextPath?: readonlystring[]
Tag/index chain when the element was inside a sequence item; omitted at the root. Built by the same descent as DeidentifiedAttribute.contextPath and carrying the same caveat: each segment's tag is read off the wire. Read that field's note before logging this one.
Type Aliases
AppliedAction
AppliedAction =
"removed"|"emptied"|"dummied"|"uid-remapped"|"cleaned"|"kept"
What deidentify actually did to one attribute - the concrete outcome of the
resolved Annex E action.
removed- the element was deleted (X).emptied- replaced with a zero-length value (Z).dummied- replaced with a non-identifying dummy of compatible VR (D).uid-remapped- UID(s) replaced with internally-consistent UIDs (U).cleaned- conservatively blanked because a safe similar-meaning value cannot be synthesised at the metadata layer (C; see known limitations).kept- retained, either by an active Retain option or because the SQ was kept and its items cleaned recursively.
Example
import { deidentify, parseDicom, type AppliedAction } from "@cosyte/dicom";
const { report } = deidentify(parseDicom(buf));
const removed = report.attributes.filter((a) => a.applied === ("removed" satisfies AppliedAction));
DeidentifyErrorCode
DeidentifyErrorCode = typeof
DEIDENTIFY_ERROR_CODES[keyof typeofDEIDENTIFY_ERROR_CODES]
One of the DEIDENTIFY_ERROR_CODES values.
Example
import { DeidentifyError, type DeidentifyErrorCode } from "@cosyte/dicom";
const code: DeidentifyErrorCode = "INVALID_OPTIONS";
throw new DeidentifyError("unknown retain option", code);
DeidentifyOption
DeidentifyOption =
Exclude<AnnexEOption,"CleanPixelData"|"CleanRecognizableVisual">
The PS3.15 Annex E option sets deidentify honours - the
metadata-affecting columns of Table E.1-1. The two pixel-level options
(CleanPixelData §E.3.1, CleanRecognizableVisual §E.3.2) are deliberately
excluded: this is a metadata-only de-identifier and cannot inspect pixels
(deferred to @cosyte/dicom-pixel). When pixel data is present it always
warns rather than claiming the image is clean.
§E.3.6 is two Options, and each has its own name here. Table E.1-1 gives
them separate columns: Rtn. Long. Full Dates (keep dates and times as they
are) and Rtn. Long. Modif. Dates (keep them only as modified values).
RetainLongitudinalTemporalcarries the full-dates column, the less protective branch: where the two columns differ, it saysK(keep the real value). Activate it only when real dates are genuinely required.RetainLongitudinalTemporalModifiedDatescarries the modified-dates column, which saysC(clean it) on every row where the two differ, and makes the run write(0028,0303) = MODIFIED.
The two are mutually exclusive: §E.3.6 defines them as alternatives, and a call naming both is rejected with DeidentifyError. Leave both off and the Basic Profile action applies, which removes or empties dates.
🩺 Date modification is the caller's, on both branches. This library
applies the column and writes the declaration; it shifts, aggregates and
transforms nothing, so a MODIFIED it writes is true only if you performed
the transformation §E.3.6 describes. report.warnings carries
DICOM_DEIDENT_DATES_NOT_TRANSFORMED on every run that activates the
modified-dates Option, saying exactly that.
Example
const retain: DeidentifyOption[] = ["RetainLongitudinalTemporal", "RetainSafePrivate"];
DicomValue
DicomValue = {
kind:"empty"; } | {kind:"text";value:string;warnings?: readonlyDicomParseWarning[]; } | {kind:"strings";values: readonlystring[];warnings?: readonlyDicomParseWarning[]; } | {kind:"personName";values: readonlyPersonName[];warnings?: readonlyDicomParseWarning[]; } | {kind:"numbers";values: readonlynumber[]; } | {kind:"bigints";values: readonlybigint[]; } | {kind:"attributeTags";values: readonlyTag[]; } | {kind:"decimalString";values: readonly (number|null)[];warnings?: readonlyDicomParseWarning[]; } | {kind:"integerString";values: readonly (number|null)[];warnings?: readonlyDicomParseWarning[]; } | {kind:"dates";values: readonlyDicomDate[];warnings?: readonlyDicomParseWarning[]; } | {kind:"times";values: readonlyDicomTime[];warnings?: readonlyDicomParseWarning[]; } | {kind:"dateTimes";values: readonlyDicomDateTime[];warnings?: readonlyDicomParseWarning[]; } | {bytes:Buffer;kind:"binary"; } | {items: readonlyItem[];kind:"sequence"; }
The lazily-decoded value of an Element, as a discriminated union
on kind. See the module doc for the VR → kind mapping. Narrow on kind
(the switch-exhaustiveness-check lint rule keeps consumers honest).
Example
import { parseDicom } from "@cosyte/dicom";
const ds = parseDicom(buf);
const v = ds.get("00100010")?.value; // Patient's Name (PN)
if (v?.kind === "personName") {
console.log(v.values[0]?.alphabetic.familyName);
}
FatalCode
FatalCode = typeof
FATAL_CODES[keyof typeofFATAL_CODES]
Discriminant type for DicomParseError.code. Narrowing a caught error
by this code lets consumers write exhaustive switch blocks (enabled
by the switch-exhaustiveness-check lint rule) and guarantees a
typo-free comparison against the FATAL_CODES registry.
Example
import type { FatalCode } from "@cosyte/dicom";
function describe(code: FatalCode): string {
switch (code) {
case "EMPTY_INPUT":
return "input was empty";
case "NOT_DICOM_PART_10":
return "input is not a DICOM Part 10 file";
case "INVALID_FILE_META":
return "File Meta group is missing or malformed";
case "UNSUPPORTED_TRANSFER_SYNTAX":
return "Transfer Syntax UID is not supported by v1";
}
}
OffsetFrame
OffsetFrame = typeof
OFFSET_FRAMES[keyof typeofOFFSET_FRAMES]
The frame a DicomParseError.byteOffset is counted in. See
OFFSET_FRAMES.
Example
import type { OffsetFrame } from "@cosyte/dicom";
function indexable(frame: OffsetFrame): boolean {
// Only the root frame's offsets index the buffer the caller passed in.
switch (frame) {
case "input":
return true;
case "inflated-dataset":
case "value-slice":
return false;
}
}
OnWarningCallback
OnWarningCallback = (
warning) =>void
Synchronous callback invoked once per Tier-2 warning emitted during parse.
The callback fires AFTER the warning has been
pushed to ctx.warnings; if the callback throws, the parser silently
swallows the exception and continues (mirrors @cosyte/hl7 sibling).
Parameters
warning
Returns
void
Example
import type { OnWarningCallback } from "@cosyte/dicom";
const onWarning: OnWarningCallback = (w) => {
if (w.code === "DICOM_MISSING_PREAMBLE") {
// ...
}
};
ProfilePrivateTags
ProfilePrivateTags =
Readonly<Record<string,PrivateTagDefinition>>
One vendor's private-dictionary overlay as authored: canonical
"GGGGXXLL" key → definition. Case-insensitive on input; normalized to
uppercase on store.
Example
import type { ProfilePrivateTags } from "@cosyte/dicom";
const csa: ProfilePrivateTags = {
"0029XX10": { vr: "OB", keyword: "CSAImageHeaderInfo", name: "CSA Image Header Info" },
};
SerializeErrorCode
SerializeErrorCode = typeof
SERIALIZE_ERROR_CODES[keyof typeofSERIALIZE_ERROR_CODES]
Discriminant for DicomSerializeError.code, enabling exhaustive
switch narrowing (the switch-exhaustiveness-check lint rule).
Example
import type { SerializeErrorCode } from "@cosyte/dicom";
function describe(code: SerializeErrorCode): string {
switch (code) {
case "MISSING_TRANSFER_SYNTAX":
return "dataset has no Transfer Syntax UID to serialize under";
case "UNSUPPORTED_TRANSFER_SYNTAX":
return "Transfer Syntax UID is neither native nor a section A.4 one";
case "INVALID_ENCAPSULATED_PIXEL_DATA":
return "top-level Pixel Data is not a section A.4 fragment stream";
case "DIRECTORY_OFFSET_UNRESOLVED":
return "a DICOMDIR offset names no Directory Record the dataset holds";
case "DIRECTORY_OFFSET_DEFLATED":
return "a Deflated DICOMDIR carries a non-zero Directory Record offset";
}
}
ValueErrorCode
ValueErrorCode = typeof
VALUE_ERROR_CODES[keyof typeofVALUE_ERROR_CODES]
Discriminant for DicomValueError.code, enabling exhaustive
switch narrowing (the switch-exhaustiveness-check lint rule).
Example
import type { ValueErrorCode } from "@cosyte/dicom";
function describe(code: ValueErrorCode): string {
switch (code) {
case "FRAME_INDEX_OUT_OF_RANGE":
return "frame index outside [0, numberOfFrames)";
case "MISSING_REQUIRED_FUNCTIONAL_GROUP":
return "enhanced object lacks a required geometry macro";
}
}
WarningCode
WarningCode = typeof
WARNING_CODES[keyof typeofWARNING_CODES]
Discriminant type for DicomParseWarning.code. Narrowing a warning by
this code lets consumers write exhaustive switch blocks (enabled by
the switch-exhaustiveness-check lint rule) and guarantees a typo-free
comparison against the WARNING_CODES registry.
Example
import type { DicomParseWarning, WarningCode } from "@cosyte/dicom";
function describe(w: DicomParseWarning): string {
const code: WarningCode = w.code;
if (code === "DICOM_MISSING_PREAMBLE") return "preamble missing";
return `warning: ${code}`;
}
Variables
CODING_SCHEME_OIDS
constCODING_SCHEME_OIDS:object
Canonical coding-scheme OIDs (PS3.16 §8). Only the four standard
designators are mapped. Legacy SNOMED designators (SRT / SNM3 /
99SDM) are intentionally absent: their code values differ from SCT
(CP-730), so resolving them to the SNOMED OID would imply a false
equality.
Type Declaration
DCM
readonlyDCM:"1.2.840.10008.2.16.4"="1.2.840.10008.2.16.4"
LN
readonlyLN:"2.16.840.1.113883.6.1"="2.16.840.1.113883.6.1"
SCT
readonlySCT:"2.16.840.1.113883.6.96"="2.16.840.1.113883.6.96"
UCUM
readonlyUCUM:"2.16.840.1.113883.6.8"="2.16.840.1.113883.6.8"
Example
import { CODING_SCHEME_OIDS } from "@cosyte/dicom";
CODING_SCHEME_OIDS.SCT; // "2.16.840.1.113883.6.96"
DEFAULT_UID_ROOT
constDEFAULT_UID_ROOT:"2.25"="2.25"
DICOM 2.25 UUID-derived root (PS3.5 §B.2) - no registration required.
Example
import { makeUidRemapper, DEFAULT_UID_ROOT } from "@cosyte/dicom";
const remap = makeUidRemapper(DEFAULT_UID_ROOT);
remap.map("1.2.840.113619.2.55.3").startsWith("2.25."); // true
DEIDENTIFICATION_METHOD_CODES
constDEIDENTIFICATION_METHOD_CODES:object
PS3.16 2026d CID 7050 "De-identification Method" (context group version
20170914, UID 1.2.840.10008.6.1.925), with the code deidentify() records
for the Profile and for each DeidentifyOption.
rows- all thirteen rows, in the context group's own order.profile-113100Basic Application Confidentiality Profile, written by every run.options- the code written when that Option was active for the run. An Option "ran" when it was active: the Annex E column it selects is resolved off the option set, whether or not the object carried an attribute it acts on.113107therefore says the modified-dates column was resolved, not that any date was shifted; this library transforms no date, and every such run carriesDICOM_DEIDENT_DATES_NOT_TRANSFORMEDonreport.warnings.
113101 and 113102 are in rows and are never written: they name the two
pixel-level Options, which this metadata-only layer does not perform.
Type Declaration
contextGroupUid
readonlycontextGroupUid:"1.2.840.10008.6.1.925"
contextGroupVersion
readonlycontextGroupVersion:"20170914"
options
readonlyoptions:Readonly<Record<DeidentifyOption,DeidentificationMethodCode>>
profile
readonlyprofile:DeidentificationMethodCode
rows
readonlyrows: readonlyDeidentificationMethodCode[]
Example
import { DEIDENTIFICATION_METHOD_CODES } from "@cosyte/dicom";
DEIDENTIFICATION_METHOD_CODES.contextGroupUid; // "1.2.840.10008.6.1.925"
DEIDENTIFICATION_METHOD_CODES.options.RetainUIDs.codeValue; // "113110"
DEIDENTIFICATION_METHOD_CODES.rows.length; // 13
DEIDENTIFY_ERROR_CODES
constDEIDENTIFY_ERROR_CODES:Readonly<{INVALID_OPTIONS:"INVALID_OPTIONS";UNSUPPORTED_TRANSFER_SYNTAX:"UNSUPPORTED_TRANSFER_SYNTAX"; }>
Stable codes for DeidentifyError.
INVALID_OPTIONS: an author-time misconfiguration of the call itself (an unknown Retain option, both PS3.15 §E.3.6 temporal Options at once, a malformeduidRoot).UNSUPPORTED_TRANSFER_SYNTAX: the Dataset's File Meta Transfer Syntax UID is one this de-identifier refuses to act on, whatever the options. Today that is exactly the four JPIP Referenced syntaxes of PS3.5 2026c sections A.6, A.7, A.11 and A.12: such an object references its pixels through Pixel Data Provider URL(0028,7FE0), which has no PS3.15 Table E.1-1 row, so a de-identified copy would keep the URL by omission. Nothing is de-identified and no dataset or report is returned.
Example
import { DEIDENTIFY_ERROR_CODES } from "@cosyte/dicom";
DEIDENTIFY_ERROR_CODES.INVALID_OPTIONS; // "INVALID_OPTIONS"
DEIDENTIFY_ERROR_CODES.UNSUPPORTED_TRANSFER_SYNTAX; // "UNSUPPORTED_TRANSFER_SYNTAX"
DEIDENTIFY_OPTIONS
constDEIDENTIFY_OPTIONS: readonlyDeidentifyOption[]
The metadata option-set names, frozen for runtime validation.
Example
import { DEIDENTIFY_OPTIONS } from "@cosyte/dicom";
DEIDENTIFY_OPTIONS.includes("RetainUIDs"); // true
FATAL_CODES
constFATAL_CODES:object
Stable string codes for every Tier-3 fatal the parser may throw.
Locked at four codes per PROJECT.md "Fatal errors only for unrecoverable
structural corruption": anything less severe MUST be a Tier-2 warning
(see ./warnings.ts). Consumers narrow on err.code to react to
specific structural failures.
Type Declaration
EMPTY_INPUT
readonlyEMPTY_INPUT:"EMPTY_INPUT"="EMPTY_INPUT"
INVALID_FILE_META
readonlyINVALID_FILE_META:"INVALID_FILE_META"="INVALID_FILE_META"
NOT_DICOM_PART_10
readonlyNOT_DICOM_PART_10:"NOT_DICOM_PART_10"="NOT_DICOM_PART_10"
UNSUPPORTED_TRANSFER_SYNTAX
readonlyUNSUPPORTED_TRANSFER_SYNTAX:"UNSUPPORTED_TRANSFER_SYNTAX"="UNSUPPORTED_TRANSFER_SYNTAX"
Example
import { parseDicom, FATAL_CODES, DicomParseError } from "@cosyte/dicom";
try {
parseDicom(Buffer.alloc(0));
} catch (err) {
if (err instanceof DicomParseError && err.code === FATAL_CODES.EMPTY_INPUT) {
// handle empty input
}
}
OFFSET_FRAMES
constOFFSET_FRAMES:object
The coordinate systems a byteOffset this parser publishes can be counted
in.
A byte offset is a number and a number alone says nothing about where its
zero is. This parser reads a Data Set out of three different buffers over one
parseDicom call, so the same small integer means three different things
depending on which one is being read - and until DICOM-DIAGNOSTIC-PHI- RESIDUALS closed it, nothing on the thrown error said which. A consumer
cutting input.subarray(err.byteOffset, err.byteOffset + 16) to see what
upset the parser was, inside a Sequence Item, cutting an unrelated element -
the exact defect the { strict: true } snippet itself was once fixed for.
A frame NAME is published; a frame ORIGIN is not, and that asymmetry is
deliberate. The name is drawn from the closed set below, which the parser
chooses and no sender can influence, and that membership is the whole of its
bound. An origin has no such table: it is a position reached by summing the
declared lengths that led to it, so two of them differ by a wire field, and a
message that already publishes byteOffset would be one number short of
one. That is a weaker argument than an impossibility and is stated as
weaker, since the library publishes positions freely in the "input"
frame; a graded pass said so. The item asked for the frame to be NAMED, and
nothing here needs the origin, so the cheap side of the trade is taken.
Type Declaration
INFLATED_DATASET
readonlyINFLATED_DATASET:"inflated-dataset"="inflated-dataset"
Byte 0 is byte 0 of the inflated Data Set of a Deflated Explicit VR LE
object (1.2.840.10008.1.2.1.99). The compressed input holds no such
byte, so the offset does not index it at any scale.
INPUT
readonlyINPUT:"input"="input"
Byte 0 is byte 0 of the buffer handed to parseDicom. The only frame in
which indexing the caller's own input by byteOffset is meaningful.
VALUE_SLICE
readonlyVALUE_SLICE:"value-slice"="value-slice"
Byte 0 is byte 0 of a slice this parser cut from inside a Value Field: a
defined-length Sequence Item's value, or an SQ/UN value handed to a
descent. Where that slice begins is deliberately not published - see
this table's own note.
Example
import { parseDicom, DicomParseError, OFFSET_FRAMES } from "@cosyte/dicom";
try {
parseDicom(buffer);
} catch (err) {
if (err instanceof DicomParseError && err.offsetFrame === OFFSET_FRAMES.INPUT) {
// Only here is `err.byteOffset` an index into the buffer you passed in.
console.error(buffer.subarray(err.byteOffset, err.byteOffset + 16));
}
}
profiles
constprofiles:Readonly<{ge:Profile;lenient:Profile;philips:Profile;siemens:Profile;strict:Profile; }>
Frozen namespace of every built-in profile: three vendor private-dictionary
overlays (ge, siemens, philips) and two posture presets (strict,
lenient). Pass one straight to parseDicom.
Example
import { parseDicom, profiles } from "@cosyte/dicom";
const ds = parseDicom(buf, { profile: profiles.siemens });
console.log(profiles.siemens.describe?.());
SERIALIZE_ERROR_CODES
constSERIALIZE_ERROR_CODES:object
Stable string codes the serializer may throw. Narrow on DicomSerializeError.code to react to a specific failure.
Type Declaration
DIRECTORY_OFFSET_DEFLATED
readonlyDIRECTORY_OFFSET_DEFLATED:"DIRECTORY_OFFSET_DEFLATED"="DIRECTORY_OFFSET_DEFLATED"
DIRECTORY_OFFSET_UNRESOLVED
readonlyDIRECTORY_OFFSET_UNRESOLVED:"DIRECTORY_OFFSET_UNRESOLVED"="DIRECTORY_OFFSET_UNRESOLVED"
INVALID_ENCAPSULATED_PIXEL_DATA
readonlyINVALID_ENCAPSULATED_PIXEL_DATA:"INVALID_ENCAPSULATED_PIXEL_DATA"="INVALID_ENCAPSULATED_PIXEL_DATA"
MISSING_TRANSFER_SYNTAX
readonlyMISSING_TRANSFER_SYNTAX:"MISSING_TRANSFER_SYNTAX"="MISSING_TRANSFER_SYNTAX"
UNSUPPORTED_TRANSFER_SYNTAX
readonlyUNSUPPORTED_TRANSFER_SYNTAX:"UNSUPPORTED_TRANSFER_SYNTAX"="UNSUPPORTED_TRANSFER_SYNTAX"
Example
import { SERIALIZE_ERROR_CODES } from "@cosyte/dicom";
SERIALIZE_ERROR_CODES.MISSING_TRANSFER_SYNTAX; // "MISSING_TRANSFER_SYNTAX"
VALUE_ERROR_CODES
constVALUE_ERROR_CODES:object
Stable string codes the domain helpers may throw. Narrow on DicomValueError.code to react to a specific contract violation.
Type Declaration
FRAME_INDEX_OUT_OF_RANGE
readonlyFRAME_INDEX_OUT_OF_RANGE:"FRAME_INDEX_OUT_OF_RANGE"="FRAME_INDEX_OUT_OF_RANGE"
MISSING_REQUIRED_FUNCTIONAL_GROUP
readonlyMISSING_REQUIRED_FUNCTIONAL_GROUP:"MISSING_REQUIRED_FUNCTIONAL_GROUP"="MISSING_REQUIRED_FUNCTIONAL_GROUP"
Example
import { VALUE_ERROR_CODES } from "@cosyte/dicom";
VALUE_ERROR_CODES.FRAME_INDEX_OUT_OF_RANGE; // "FRAME_INDEX_OUT_OF_RANGE"
VERSION
constVERSION:string="0.1.0"
Package version string for @cosyte/dicom. Synchronized with package.json#version by
scripts/sync-version.mjs, which the version script runs after changeset version
so the bump and this constant land in the same "Version Packages" commit.
Stays on the uniform 0.0.x-until-first-alpha ladder (locked across the @cosyte/* suite):
patch bumps via Changesets through pre-alpha, with no 0.1.0 milestone bump.
Example
import { VERSION } from "@cosyte/dicom";
console.log(`@cosyte/dicom v${VERSION}`);
WARNING_CODES
constWARNING_CODES:object
Stable string codes for every Tier-2 warning the parser may emit.
The registry is frozen via as const so TypeScript infers the exact
string-literal union for WarningCode - there is zero runtime cost and
no magic-string comparisons for consumers. Reserved-but-not-emitted
codes carry inline comments documenting which stage activates them; the
union is declared whole so the schema is stable for consumers.
Type Declaration
DICOM_BOM_IN_TEXT_VR
readonlyDICOM_BOM_IN_TEXT_VR:"DICOM_BOM_IN_TEXT_VR"="DICOM_BOM_IN_TEXT_VR"
DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED
readonlyDICOM_BURNED_IN_ANNOTATION_NOT_REMOVED:"DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED"="DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED"
DICOM_CHARSET_AMBIGUOUS_SEPARATOR
readonlyDICOM_CHARSET_AMBIGUOUS_SEPARATOR:"DICOM_CHARSET_AMBIGUOUS_SEPARATOR"="DICOM_CHARSET_AMBIGUOUS_SEPARATOR"
DICOM_CHARSET_BYTES_UNDECODABLE
readonlyDICOM_CHARSET_BYTES_UNDECODABLE:"DICOM_CHARSET_BYTES_UNDECODABLE"="DICOM_CHARSET_BYTES_UNDECODABLE"
DICOM_CHARSET_ESCAPE_UNDECLARED
readonlyDICOM_CHARSET_ESCAPE_UNDECLARED:"DICOM_CHARSET_ESCAPE_UNDECLARED"="DICOM_CHARSET_ESCAPE_UNDECLARED"
DICOM_CHARSET_EXTENSION_NOT_RESET
readonlyDICOM_CHARSET_EXTENSION_NOT_RESET:"DICOM_CHARSET_EXTENSION_NOT_RESET"="DICOM_CHARSET_EXTENSION_NOT_RESET"
DICOM_DA_LEGACY_FORMAT
readonlyDICOM_DA_LEGACY_FORMAT:"DICOM_DA_LEGACY_FORMAT"="DICOM_DA_LEGACY_FORMAT"
DICOM_DEIDENT_DATES_NOT_TRANSFORMED
readonlyDICOM_DEIDENT_DATES_NOT_TRANSFORMED:"DICOM_DEIDENT_DATES_NOT_TRANSFORMED"="DICOM_DEIDENT_DATES_NOT_TRANSFORMED"
DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED
readonlyDICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED:"DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED"="DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED"
DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED
readonlyDICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED:"DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED"="DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED"
DICOM_DEIDENT_FILE_META_REPLACED
readonlyDICOM_DEIDENT_FILE_META_REPLACED:"DICOM_DEIDENT_FILE_META_REPLACED"="DICOM_DEIDENT_FILE_META_REPLACED"
DICOM_DEIDENT_GROUP_0004_REMOVED
readonlyDICOM_DEIDENT_GROUP_0004_REMOVED:"DICOM_DEIDENT_GROUP_0004_REMOVED"="DICOM_DEIDENT_GROUP_0004_REMOVED"
DICOM_DEIDENT_METHOD_CODES_PRIOR_REPLACED
readonlyDICOM_DEIDENT_METHOD_CODES_PRIOR_REPLACED:"DICOM_DEIDENT_METHOD_CODES_PRIOR_REPLACED"="DICOM_DEIDENT_METHOD_CODES_PRIOR_REPLACED"
DICOM_DEIDENT_METHOD_CODES_PRIOR_RETAINED
readonlyDICOM_DEIDENT_METHOD_CODES_PRIOR_RETAINED:"DICOM_DEIDENT_METHOD_CODES_PRIOR_RETAINED"="DICOM_DEIDENT_METHOD_CODES_PRIOR_RETAINED"
DICOM_DEIDENT_METHOD_NOT_ADDED
readonlyDICOM_DEIDENT_METHOD_NOT_ADDED:"DICOM_DEIDENT_METHOD_NOT_ADDED"="DICOM_DEIDENT_METHOD_NOT_ADDED"
DICOM_DEIDENT_METHOD_NOT_LO
readonlyDICOM_DEIDENT_METHOD_NOT_LO:"DICOM_DEIDENT_METHOD_NOT_LO"="DICOM_DEIDENT_METHOD_NOT_LO"
DICOM_DEIDENT_METHOD_PRIOR_RETAINED
readonlyDICOM_DEIDENT_METHOD_PRIOR_RETAINED:"DICOM_DEIDENT_METHOD_PRIOR_RETAINED"="DICOM_DEIDENT_METHOD_PRIOR_RETAINED"
DICOM_DEIDENT_METHOD_VALUE_OVER_LENGTH
readonlyDICOM_DEIDENT_METHOD_VALUE_OVER_LENGTH:"DICOM_DEIDENT_METHOD_VALUE_OVER_LENGTH"="DICOM_DEIDENT_METHOD_VALUE_OVER_LENGTH"
DICOM_DEIDENT_PRIVATE_CARRIER_NOT_AUDITABLE
readonlyDICOM_DEIDENT_PRIVATE_CARRIER_NOT_AUDITABLE:"DICOM_DEIDENT_PRIVATE_CARRIER_NOT_AUDITABLE"="DICOM_DEIDENT_PRIVATE_CARRIER_NOT_AUDITABLE"
DICOM_DEIDENT_PRIVATE_DECLARATION_NOT_RESOLVED
readonlyDICOM_DEIDENT_PRIVATE_DECLARATION_NOT_RESOLVED:"DICOM_DEIDENT_PRIVATE_DECLARATION_NOT_RESOLVED"="DICOM_DEIDENT_PRIVATE_DECLARATION_NOT_RESOLVED"
DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE
readonlyDICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE:"DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE"="DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE"
DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE
readonlyDICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE:"DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE"="DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE"
DICOM_DEIDENT_UNREGISTERED_ELEMENT_REMOVED
readonlyDICOM_DEIDENT_UNREGISTERED_ELEMENT_REMOVED:"DICOM_DEIDENT_UNREGISTERED_ELEMENT_REMOVED"="DICOM_DEIDENT_UNREGISTERED_ELEMENT_REMOVED"
DICOM_DIRECTORY_OFFSET_DEFLATED
readonlyDICOM_DIRECTORY_OFFSET_DEFLATED:"DICOM_DIRECTORY_OFFSET_DEFLATED"="DICOM_DIRECTORY_OFFSET_DEFLATED"
DICOM_DIRECTORY_OFFSET_MALFORMED
readonlyDICOM_DIRECTORY_OFFSET_MALFORMED:"DICOM_DIRECTORY_OFFSET_MALFORMED"="DICOM_DIRECTORY_OFFSET_MALFORMED"
DICOM_DIRECTORY_OFFSET_UNRESOLVED
readonlyDICOM_DIRECTORY_OFFSET_UNRESOLVED:"DICOM_DIRECTORY_OFFSET_UNRESOLVED"="DICOM_DIRECTORY_OFFSET_UNRESOLVED"
DICOM_DIRECTORY_RECORD_REVISITED
readonlyDICOM_DIRECTORY_RECORD_REVISITED:"DICOM_DIRECTORY_RECORD_REVISITED"="DICOM_DIRECTORY_RECORD_REVISITED"
DICOM_DT_NONSTANDARD_OFFSET
readonlyDICOM_DT_NONSTANDARD_OFFSET:"DICOM_DT_NONSTANDARD_OFFSET"="DICOM_DT_NONSTANDARD_OFFSET"
DICOM_DUPLICATE_FILE_META_ELEMENT
readonlyDICOM_DUPLICATE_FILE_META_ELEMENT:"DICOM_DUPLICATE_FILE_META_ELEMENT"="DICOM_DUPLICATE_FILE_META_ELEMENT"
DICOM_DUPLICATE_TAG_IN_DATA_SET
readonlyDICOM_DUPLICATE_TAG_IN_DATA_SET:"DICOM_DUPLICATE_TAG_IN_DATA_SET"="DICOM_DUPLICATE_TAG_IN_DATA_SET"
DICOM_EMPTY_ITEM_IN_SEQUENCE
readonlyDICOM_EMPTY_ITEM_IN_SEQUENCE:"DICOM_EMPTY_ITEM_IN_SEQUENCE"="DICOM_EMPTY_ITEM_IN_SEQUENCE"
DICOM_FILE_META_GROUP_LENGTH_MISMATCH
readonlyDICOM_FILE_META_GROUP_LENGTH_MISMATCH:"DICOM_FILE_META_GROUP_LENGTH_MISMATCH"="DICOM_FILE_META_GROUP_LENGTH_MISMATCH"
DICOM_FILE_META_GROUP_LENGTH_MISSING
readonlyDICOM_FILE_META_GROUP_LENGTH_MISSING:"DICOM_FILE_META_GROUP_LENGTH_MISSING"="DICOM_FILE_META_GROUP_LENGTH_MISSING"
DICOM_GROUP_LENGTH_IN_DATASET
readonlyDICOM_GROUP_LENGTH_IN_DATASET:"DICOM_GROUP_LENGTH_IN_DATASET"="DICOM_GROUP_LENGTH_IN_DATASET"
DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR
readonlyDICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR:"DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR"="DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR"
DICOM_IS_NONINTEGER_VALUE
readonlyDICOM_IS_NONINTEGER_VALUE:"DICOM_IS_NONINTEGER_VALUE"="DICOM_IS_NONINTEGER_VALUE"
DICOM_ITEM_CROSSES_SEQUENCE_END
readonlyDICOM_ITEM_CROSSES_SEQUENCE_END:"DICOM_ITEM_CROSSES_SEQUENCE_END"="DICOM_ITEM_CROSSES_SEQUENCE_END"
DICOM_MISSING_PREAMBLE
readonlyDICOM_MISSING_PREAMBLE:"DICOM_MISSING_PREAMBLE"="DICOM_MISSING_PREAMBLE"
DICOM_NON_ASCII_IN_ASCII_VR
readonlyDICOM_NON_ASCII_IN_ASCII_VR:"DICOM_NON_ASCII_IN_ASCII_VR"="DICOM_NON_ASCII_IN_ASCII_VR"
DICOM_NONZERO_RESERVED_BYTES
readonlyDICOM_NONZERO_RESERVED_BYTES:"DICOM_NONZERO_RESERVED_BYTES"="DICOM_NONZERO_RESERVED_BYTES"
DICOM_ODD_LENGTH_VALUE_PADDED
readonlyDICOM_ODD_LENGTH_VALUE_PADDED:"DICOM_ODD_LENGTH_VALUE_PADDED"="DICOM_ODD_LENGTH_VALUE_PADDED"
DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED
readonlyDICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED:"DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED"="DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED"
DICOM_PIXEL_DATA_LENGTH_MISMATCH
readonlyDICOM_PIXEL_DATA_LENGTH_MISMATCH:"DICOM_PIXEL_DATA_LENGTH_MISMATCH"="DICOM_PIXEL_DATA_LENGTH_MISMATCH"
DICOM_PRIVATE_CREATOR_UNKNOWN
readonlyDICOM_PRIVATE_CREATOR_UNKNOWN:"DICOM_PRIVATE_CREATOR_UNKNOWN"="DICOM_PRIVATE_CREATOR_UNKNOWN"
DICOM_PRIVATE_TAG_NO_CREATOR
readonlyDICOM_PRIVATE_TAG_NO_CREATOR:"DICOM_PRIVATE_TAG_NO_CREATOR"="DICOM_PRIVATE_TAG_NO_CREATOR"
DICOM_SQ_NOT_DESCENDED
readonlyDICOM_SQ_NOT_DESCENDED:"DICOM_SQ_NOT_DESCENDED"="DICOM_SQ_NOT_DESCENDED"
DICOM_TRAILING_NULL_IN_TEXT_VR
readonlyDICOM_TRAILING_NULL_IN_TEXT_VR:"DICOM_TRAILING_NULL_IN_TEXT_VR"="DICOM_TRAILING_NULL_IN_TEXT_VR"
DICOM_UI_TRAILING_SPACE
readonlyDICOM_UI_TRAILING_SPACE:"DICOM_UI_TRAILING_SPACE"="DICOM_UI_TRAILING_SPACE"
DICOM_UN_PARSED_AS_SQ
readonlyDICOM_UN_PARSED_AS_SQ:"DICOM_UN_PARSED_AS_SQ"="DICOM_UN_PARSED_AS_SQ"
DICOM_UNDEFINED_LENGTH_IN_EXPLICIT_VR
readonlyDICOM_UNDEFINED_LENGTH_IN_EXPLICIT_VR:"DICOM_UNDEFINED_LENGTH_IN_EXPLICIT_VR"="DICOM_UNDEFINED_LENGTH_IN_EXPLICIT_VR"
DICOM_UNSUPPORTED_CHARSET
readonlyDICOM_UNSUPPORTED_CHARSET:"DICOM_UNSUPPORTED_CHARSET"="DICOM_UNSUPPORTED_CHARSET"
DICOM_VR_MISMATCH
readonlyDICOM_VR_MISMATCH:"DICOM_VR_MISMATCH"="DICOM_VR_MISMATCH"
Example
import { parseDicom, WARNING_CODES } from "@cosyte/dicom";
const ds = parseDicom(buf);
if (ds.warnings.some((w) => w.code === WARNING_CODES.DICOM_MISSING_PREAMBLE)) {
// handle bare File Meta input
}
Functions
codingSchemeOid()
codingSchemeOid(
designator):string|undefined
Resolve a coding-scheme designator to its canonical OID, or undefined
for any non-standard / legacy designator (including SRT/SNM3/99SDM,
which are NOT treated as SCT).
Parameters
designator
string | undefined
Returns
string | undefined
Example
import { codingSchemeOid } from "@cosyte/dicom";
codingSchemeOid("UCUM"); // "2.16.840.1.113883.6.8"
codingSchemeOid("SRT"); // undefined - not SCT (CP-730)
decodeElementValue()
decodeElementValue(
element):DicomValue
Decode an Element's value to a typed DicomValue. Pure +
fail-safe: it never throws and never coerces a malformed value to a
plausible-but-wrong one. Called lazily (and memoized) by Element.value.
Parameters
element
Returns
Example
import { parseDicom } from "@cosyte/dicom";
const ds = parseDicom(buf);
const v = ds.get("00280010")?.value; // Rows (US)
if (v?.kind === "numbers") console.log(v.values[0]);
decodeText()
decodeText(
bytes,terms):string
Decode bytes to a string under the Specific Character Set terms, never throwing.
With more than one term, and a Value 1 other than ISO_IR 192, GB18030 or
GBK, the bytes decode with ISO 2022 code extensions (PS3.5 section
6.1.2.5): every PS3.3 Table C.12-3 and C.12-4 escape sequence switches G0 or
G1, no escape byte reaches the string, and the Value 1 designations come back
after each CR, LF or FF, which are the reset points of a text VR. Bytes no
designated set decodes read as U+FFFD. Only the string is returned here;
Element.value carries the warnings for an element's value.
Every other term list decodes under resolveDecoderLabel's label: an unsupported label falls back to UTF-8, then to Latin-1 (1:1, cannot fail).
Parameters
bytes
Buffer
terms
readonly string[] | undefined
Returns
string
Example
import { decodeText } from "@cosyte/dicom";
const s = decodeText(buf, ["ISO_IR 192"]); // UTF-8
// ESC $ B, the JIS X 0208 bytes of one character, ESC ( B
decodeText(Buffer.from([0x1b, 0x24, 0x42, 0x3b, 0x33, 0x1b, 0x28, 0x42]), ["", "ISO 2022 IR 87"]);
defineProfile()
defineProfile(
opts):Profile
Build a frozen Profile from a validated options object.
Parameters
opts
Returns
Example
import { defineProfile, parseDicom } from "@cosyte/dicom";
const acme = defineProfile({
name: "acme",
description: "ACME PACS quirks",
privateTags: {
"ACME_PRIV_01": {
"0019XX10": { vr: "DS", keyword: "AcmeDose", name: "ACME Dose" },
},
},
});
const ds = parseDicom(buf, { profile: acme });
console.log(acme.describe?.());
deidentify()
deidentify(
ds,options?):DeidentifyResult<Dataset>
De-identify a Dataset per PS3.15 Annex E - the Basic Application Level
Confidentiality Profile, plus any Retain/Clean Options passed in retain.
Pure: ds is never mutated. Returns a fresh dataset and a
DeidentifyReport of tags, keywords, action codes, warnings and the UID
map. Everything in it is composed from static tables except uidMap, whose
keys are the source UIDs the file carried: treat that field as PHI.
The returned Data Set says what this run did to its dates as well as to its
identity: (0028,0303) Longitudinal Temporal Information Modified carries
UNMODIFIED when RetainLongitudinalTemporal was active, MODIFIED when
RetainLongitudinalTemporalModifiedDates was, and REMOVED under neither,
replacing any value the source carried at that tag. It is not on
DeidentifyReport - like (0012,0062), it is a statement the object
makes about itself, and the report's shape is unchanged by it. 🩺 A
MODIFIED means the modified-dates column was resolved; you perform the
date transformation §E.3.6 describes, and report.warnings carries
DICOM_DEIDENT_DATES_NOT_TRANSFORMED saying so.
A JPIP Referenced object is refused, not de-identified. When
ds.fileMeta.transferSyntaxUID is one of the four JPIP Referenced syntaxes
(PS3.5 2026c sections A.6, A.7, A.11 and A.12), the call throws before it
reads the options or the Data Set, because that object's Pixel Data Provider
URL (0028,7FE0) has no PS3.15 Table E.1-1 action and would otherwise be kept
by omission. The refusal keys on the File Meta alone: a non-JPIP object that
carries (0028,7FE0) is de-identified like any other, and keeps it.
Parameters
ds
options?
DeidentifyOptions = {}
Returns
Throws
DeidentifyError (UNSUPPORTED_TRANSFER_SYNTAX) for a Dataset
whose File Meta Transfer Syntax UID is one of the four JPIP Referenced
syntaxes, whatever the options; its message is a fixed string. Checked
first, so it is the code such a call gets even when its options are
invalid too.
Throws
DeidentifyError (INVALID_OPTIONS) for an unknown Retain option,
for both PS3.15 §E.3.6 temporal Options in one call, or for a malformed
uidRoot.
Example
import { parseDicom, deidentify, serializeDicom } from "@cosyte/dicom";
const { dataset, report } = deidentify(parseDicom(buf));
const clean = serializeDicom(dataset); // safe to share
console.log(report.attributes.length, "attributes acted on");
isKnownCharsetTerm()
isKnownCharsetTerm(
term):boolean
true when term is a Specific Character Set defined term this build can
map to a decoder (the empty default-repertoire term counts as known).
Parameters
term
string
Returns
boolean
Example
import { isKnownCharsetTerm } from "@cosyte/dicom";
isKnownCharsetTerm("ISO_IR 192"); // true
isKnownCharsetTerm("ISO_IR 14"); // false - does not exist
makeUidRemapper()
makeUidRemapper(
root?,cache?):UidRemapper
Build a UidRemapper rooted at root, optionally seeded with (and
writing through to) a caller-owned cache for cross-call sharing.
Parameters
root?
string = DEFAULT_UID_ROOT
Dotted-decimal UID root (default DEFAULT_UID_ROOT).
cache?
Map<string, string> = ...
Source→replacement map to fill (default a fresh Map).
Returns
Throws
when root is not a valid dotted-decimal OID prefix
or leaves no room for a value component within the 64-char UID limit.
Example
import { makeUidRemapper } from "@cosyte/dicom";
const remap = makeUidRemapper();
remap.map("1.2.840.113619.2.55.3") === remap.map("1.2.840.113619.2.55.3"); // true
parseDate()
parseDate(
raw):object
Decode a single DA value. legacy is true for any tolerated
non-canonical form (retired dotted YYYY.MM.DD, or an unparseable value
such as "ANONYMIZED"), signalling the caller to emit
DICOM_DA_LEGACY_FORMAT.
Parameters
raw
string
Returns
object
legacy
legacy:
boolean
value
value:
DicomDate
Example
import { parseDate } from "@cosyte/dicom";
parseDate("20240115").value.valid; // true
parseDate("2024.01.15").legacy; // true
parseDateTime()
parseDateTime(
raw):object
Decode a single DT value. nonstandardOffset is true when a UTC
offset suffix is present but malformed / out of range, signalling the
caller to emit DICOM_DT_NONSTANDARD_OFFSET.
Parameters
raw
string
Returns
object
nonstandardOffset
nonstandardOffset:
boolean
value
value:
DicomDateTime
Example
import { parseDateTime } from "@cosyte/dicom";
parseDateTime("20240115133015+0100").value.offsetMinutes; // 60
parseDicom()
Internal
- implementation signature. Public JSDoc lives on the overloads above.
Call Signature
parseDicom(
input):Dataset
Parse a DICOM Part 10 buffer into a structural Dataset.
Lenient by default - recoverable deviations (missing preamble, File Meta
group-length mismatch, odd-length value, etc.) are pushed into
ds.warnings with stable codes from WARNING_CODES. Four unrecoverable
structural failures throw DicomParseError:
EMPTY_INPUT- emptyBuffer | Uint8Array | ArrayBuffer.NOT_DICOM_PART_10- input lacks bothDICMmagic at offset 128 and a recognizable(0002,0000)File Meta Group Length at offset 0.INVALID_FILE_META- File Meta is truncated or(0002,0010)Transfer Syntax UID is missing.UNSUPPORTED_TRANSFER_SYNTAX- Transfer Syntax UID is none of the supported ones: the four native syntaxes (1.2.840.10008.1.2,…1.2.1,…1.2.2,…1.2.1.99), every encapsulated Pixel Data syntax PS3.5 2026c section A.4 names (JPEG, JPEG-LS, JPEG 2000, HTJ2K, RLE, MPEG, HEVC, JPEG XL, Deflated Image Frame, Encapsulated Uncompressed), and the four JPIP Referenced syntaxes of sections A.6, A.7, A.11 and A.12. The SMPTE ST 2110 syntaxes and every retired UID stay refused.
An object under a section A.4 syntax is read under Explicit VR Little Endian rules, as A.4 requires: its metadata is fully readable, and its Pixel Data fragments are handed back as opaque, undecoded bytes by readPixelDataFragments. No pixel is ever decoded.
An object under a JPIP Referenced syntax is read the same way for metadata:
sections A.6 and A.11 make the Data Set Explicit VR Little Endian, and
sections A.7 and A.12 deflate that Data Set per RFC 1951, so it is inflated
first under the Deflated reader's decompression cap. Its Pixel Data Provider
URL (0028,7FE0) is returned as the UR element the file carries and is
never fetched, resolved or validated. The object is read-only here:
serializeDicom refuses to write the four JPIP syntaxes, and deidentify
refuses to de-identify an object under one of them.
Pass { strict: true } to escalate every Tier-2 warning to a thrown
DicomParseError carrying the warning code.
Parameters
input
ArrayBuffer | Buffer<ArrayBufferLike> | Uint8Array<ArrayBufferLike>
Returns
Example
import { parseDicom, WARNING_CODES, DicomParseError } from "@cosyte/dicom";
import { readFileSync } from "node:fs";
// Three input shapes: Buffer, Uint8Array, ArrayBuffer.
const bytes = readFileSync("study.dcm");
const ds1 = parseDicom(bytes);
const ds2 = parseDicom(new Uint8Array(bytes));
const ds3 = parseDicom(bytes.buffer);
// Inspect File Meta + warnings.
console.log(ds1.fileMeta?.transferSyntaxUID);
for (const w of ds1.warnings) {
if (w.code === WARNING_CODES.DICOM_MISSING_PREAMBLE) {
console.warn("bare File Meta input at offset", w.position.byteOffset);
}
}
// Strict mode + onWarning callback.
try {
parseDicom(bytes, {
strict: true,
onWarning: (w) => console.error(w.code, "at offset", w.position.byteOffset),
});
} catch (err) {
if (err instanceof DicomParseError) {
console.error(err.code, err.byteOffset, err.snippet);
}
}
Call Signature
parseDicom(
input,options):Dataset
Parse a DICOM Part 10 buffer into a structural Dataset.
Lenient by default - recoverable deviations (missing preamble, File Meta
group-length mismatch, odd-length value, etc.) are pushed into
ds.warnings with stable codes from WARNING_CODES. Four unrecoverable
structural failures throw DicomParseError:
EMPTY_INPUT- emptyBuffer | Uint8Array | ArrayBuffer.NOT_DICOM_PART_10- input lacks bothDICMmagic at offset 128 and a recognizable(0002,0000)File Meta Group Length at offset 0.INVALID_FILE_META- File Meta is truncated or(0002,0010)Transfer Syntax UID is missing.UNSUPPORTED_TRANSFER_SYNTAX- Transfer Syntax UID is none of the supported ones: the four native syntaxes (1.2.840.10008.1.2,…1.2.1,…1.2.2,…1.2.1.99), every encapsulated Pixel Data syntax PS3.5 2026c section A.4 names (JPEG, JPEG-LS, JPEG 2000, HTJ2K, RLE, MPEG, HEVC, JPEG XL, Deflated Image Frame, Encapsulated Uncompressed), and the four JPIP Referenced syntaxes of sections A.6, A.7, A.11 and A.12. The SMPTE ST 2110 syntaxes and every retired UID stay refused.
An object under a section A.4 syntax is read under Explicit VR Little Endian rules, as A.4 requires: its metadata is fully readable, and its Pixel Data fragments are handed back as opaque, undecoded bytes by readPixelDataFragments. No pixel is ever decoded.
An object under a JPIP Referenced syntax is read the same way for metadata:
sections A.6 and A.11 make the Data Set Explicit VR Little Endian, and
sections A.7 and A.12 deflate that Data Set per RFC 1951, so it is inflated
first under the Deflated reader's decompression cap. Its Pixel Data Provider
URL (0028,7FE0) is returned as the UR element the file carries and is
never fetched, resolved or validated. The object is read-only here:
serializeDicom refuses to write the four JPIP syntaxes, and deidentify
refuses to de-identify an object under one of them.
Pass { strict: true } to escalate every Tier-2 warning to a thrown
DicomParseError carrying the warning code.
Parameters
input
ArrayBuffer | Buffer<ArrayBufferLike> | Uint8Array<ArrayBufferLike>
options
Returns
Example
import { parseDicom, WARNING_CODES, DicomParseError } from "@cosyte/dicom";
import { readFileSync } from "node:fs";
// Three input shapes: Buffer, Uint8Array, ArrayBuffer.
const bytes = readFileSync("study.dcm");
const ds1 = parseDicom(bytes);
const ds2 = parseDicom(new Uint8Array(bytes));
const ds3 = parseDicom(bytes.buffer);
// Inspect File Meta + warnings.
console.log(ds1.fileMeta?.transferSyntaxUID);
for (const w of ds1.warnings) {
if (w.code === WARNING_CODES.DICOM_MISSING_PREAMBLE) {
console.warn("bare File Meta input at offset", w.position.byteOffset);
}
}
// Strict mode + onWarning callback.
try {
parseDicom(bytes, {
strict: true,
onWarning: (w) => console.error(w.code, "at offset", w.position.byteOffset),
});
} catch (err) {
if (err instanceof DicomParseError) {
console.error(err.code, err.byteOffset, err.snippet);
}
}
parsePersonName()
parsePersonName(
value):PersonName
Parse a single (already charset-decoded, pad-trimmed) PN value string into its structured PersonName form.
Parameters
value
string
Returns
Example
import { parsePersonName } from "@cosyte/dicom";
const pn = parsePersonName("Doe^Jane^^Dr^");
pn.alphabetic.familyName; // "Doe"
pn.alphabetic.namePrefix; // "Dr"
parseSpecificCharacterSet()
parseSpecificCharacterSet(
bytes): readonlystring[]
Parse a (0008,0005) Specific Character Set value (a CS, possibly
multi-valued via ``) into its trimmed defined terms.
The first value may be empty (the ISO-2022 "G0 starts as ISO_IR 6" rule);
it is preserved as "" rather than dropped so resolveDecoderLabel
can apply the default-repertoire fallback.
A component this closed table does not name is not a defined term, and it is
withheld rather than returned. This value is multi-valued on the
backslash, every component is a string a sender authored, and the result
lands on Element.specificCharacterSet, where it presents itself as an
identifier a downstream package may interpolate: that is the field, and the
delimiter, this package's measured PHI leak ran through. Decoding is
unaffected, because resolveDecoderLabel already skipped any term it
could not map and skips the withheld marker for the same reason.
Parameters
bytes
Buffer
Returns
readonly string[]
Example
import { parseSpecificCharacterSet } from "@cosyte/dicom";
// bytes for "ISO 2022 IR 6\\ISO 2022 IR 87"
const terms = parseSpecificCharacterSet(buf);
parseTime()
parseTime(
raw):object
Decode a single TM value (max 14 bytes). Precision may be truncated from
the right (HH, HHMM, HHMMSS, HHMMSS.FFFFFF).
Parameters
raw
string
Returns
object
value
value:
DicomTime
Example
import { parseTime } from "@cosyte/dicom";
parseTime("133015.5").value.fractionalSeconds; // 0.5
readCode()
readCode(
item):CodedConcept
Read the coded triplet off a code-item dataset (e.g. one item of a Code
Sequence). Every part is independently fail-safe; schemeUid is the
resolved OID for the designator when standard, else undefined.
Parameters
item
Returns
Example
import { parseDicom, readCode } from "@cosyte/dicom";
const units = parseDicom(buf).image.realWorldValueMaps?.[0]?.unitsCode;
// units?.codeValue / units?.codingSchemeDesignator / units?.codeMeaning
readPixelDataFragments()
readPixelDataFragments(
ds):PixelDataFragments|undefined
Read the Basic Offset Table and every fragment of a Data Set's encapsulated
Pixel Data (7FE0,0010) as raw bytes, without decoding anything.
Pass the root Dataset parseDicom returned: its Pixel Data is the
top-level element PS3.5 2026c section A.4 encapsulates. An Item is a
Dataset too, and is read the same way over its own elements.
undefined means this Data Set has no encapsulated fragments: Pixel Data
is absent, or it is present in native format with an explicit Value Length.
That is never an empty list and never a Basic Offset Table, so it cannot be
mistaken for an encapsulated result.
The list can be short, and the parse says so. A stream that ends with the
input before its Sequence Delimitation Item still parses, and
DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED on ds.warnings says that the
fragments read may not be all the sender wrote. Read the warnings before
treating the list as complete.
Parameters
ds
Returns
PixelDataFragments | undefined
Throws
DicomParseError with code INVALID_FILE_META when the element's
bytes are not an Item stream (an Item header cut short, an Item reaching past
the bytes, or a tag other than (FFFE,E000) / (FFFE,E0DD)). parseDicom
refuses such bytes itself, so only a hand-built Element reaches this; the
error carries no fragment byte and no length outside its 16-byte snippet.
Example
import { parseDicom, readPixelDataFragments } from "@cosyte/dicom";
const ds = parseDicom(buf); // e.g. a JPEG 2000 object
const pixels = readPixelDataFragments(ds);
if (pixels === undefined) {
// no encapsulated Pixel Data: absent, or native
} else {
for (const fragment of pixels.fragments) {
// hand `fragment` to a codec of your choice; it is the bytes as written
}
}
resolveDecoderLabel()
resolveDecoderLabel(
terms):string
Resolve the TextDecoder label to use for a value, given the dataset's
Specific Character Set terms. Prefers a multibyte/extended decoder when
the term list mixes single-byte and code-extension sets; falls back to
the default repertoire (Latin-1, lenient) when no term resolves.
decodeText decodes under this label when the list has one term, or
when Value 1 is ISO_IR 192, GB18030 or GBK. Any other multi-term list
decodes with ISO 2022 code extensions instead, whatever label this returns.
Parameters
terms
readonly string[] | undefined
Returns
string
Example
import { resolveDecoderLabel } from "@cosyte/dicom";
resolveDecoderLabel(["ISO 2022 IR 6", "ISO 2022 IR 87"]); // "iso-2022-jp"
resolveDecoderLabel(undefined); // "latin1"
serializeDicom()
serializeDicom(
ds):Buffer
Serialize a Dataset to a spec-clean DICOM Part 10 Buffer.
The dataset's transfer syntax is preserved (no transcoding): pixel-data
fragments are written back byte-for-byte, nested sequences carry the same
bytes with each Item's Data Elements in ascending tag order, and scalar values
are re-emitted with correct even-length padding and File Meta group length.
Pure function - the input Dataset is never mutated.
Element order. Every Data Set is written in ascending tag order (PS3.5
2026c §7.1, §7.5.1): the root, and the Data Set of every Item in every
Sequence the writer can walk on the wire, at every depth up to
NESTING_DEPTH_LIMIT. Items stay in their order (PS3.5 2026c §7.5) and no
value changes; only whole element spans move, and only where the parsed
items match the bytes, so the output reads back as the source did. Limits:
a tag repeated inside an Item is kept twice, in source order, so that output
still breaks PS3.5 2026c §7.1's "at most once"; a Sequence whose Item stream
cannot be walked to exactly its end, one nested past the bound, one whose
items do not match its bytes (a Sequence the parser did not descend, for
one), and any UN-carried Sequence (under Implicit VR LE that includes a
private Sequence inside an Item, even one a Profile resolved to SQ, since
a default read resolves its tag to UN) are written as read, unordered; an
element whose own bytes do not show where a reader ends it (an
undefined-length UN the parser could not read as a Sequence, or a value
missing its Sequence Delimitation Item) is written after the ascending rest
of its Data Set, because a reader takes what follows it into its value, and
so is a Sequence nested past the bound (other than a defined-length one under
Implicit VR LE), whose end the writer would have to walk past the bound to
see; and an element the parser relocated because a length lied is ordered
where it was placed, since ordering cannot recover an order the source
destroyed.
Encapsulated objects. Under every Transfer Syntax PS3.5 2026c section A.4
names (JPEG, JPEG-LS, JPEG 2000, HTJ2K, RLE and the rest: the list parseDicom
reads), the File Meta Transfer Syntax UID is kept, the Data Set is written as
Explicit VR Little Endian, and the top-level Pixel Data (7FE0,0010) is
written as OB of undefined length: the input's Basic Offset Table Item and
fragment Items byte for byte and in order, then a zero-length Sequence
Delimitation Item. Nothing is decoded, transcoded or re-framed, and no offset
table is interpreted or rebuilt. The limit sits with it: a top-level Data Set
that section A.4 does not allow is refused with
INVALID_ENCAPSULATED_PIXEL_DATA rather than repaired, which includes a
fragment stream parseDicom read with DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED,
an odd Item Length, an empty fragment, native or absent top-level Pixel Data,
and Float or Double Float Pixel Data. Pixel Data nested in a Sequence Item is
written as read.
DICOMDIR offsets. For a Dataset whose File Meta Media Storage SOP Class
UID is 1.2.840.10008.1.3.10, (0004,1200), (0004,1202) and each Directory
Record's (0004,1400) and (0004,1420) are written as the byte offset, from
the first preamble byte of the output, of the Directory Record each named when
the file was read (PS3.3 2026d Table F.3-3), whatever moved it: the rebuilt
File Meta group, the ascending order above, or deidentify(). A record is the
Item of the Directory Record Sequence (0004,1220) whose Item.fileOffset
the offset equals; nothing is found by scanning for an Item tag. A zero offset
is written as zero. Limits: the retired MRDR offset (0004,1504) is written as
read, and nothing checks record keys or (0004,1202) against the root chain.
Input contract. The writer is designed for a Dataset produced by
parseDicom: it relies on the parser's Element.rawBytes representation
(value-only for scalars and Implicit-LE defined-length SQ; full on-wire span
for Explicit SQ, undefined-length spans, encapsulated Pixel Data, and the
UN/CP-246 fallbacks). A hand-built Dataset must follow the same
convention for its bytes to be encoded correctly.
Round-trip scope. parseDicom(out) re-reads to a dataset that is equal
over the modeled surface (every dataset element + the typed
"../dataset/file-meta".FileMeta fields plus any non-modeled File Meta
elements preserved on extraElements), not a byte-exact copy of the original
file: the 128-byte preamble is normalized to zeros, the File Meta group is
rebuilt in ascending tag order (modeled fields + extraElements - see
encodeFileMeta), the Data Sets are ordered as above, odd-length values
are padded even, and retired (gggg,0000) group lengths are dropped.
Parameters
ds
Returns
Buffer
Throws
DicomSerializeError with code MISSING_TRANSFER_SYNTAX when
the dataset has no File Meta Transfer Syntax UID,
UNSUPPORTED_TRANSFER_SYNTAX when that UID is neither one of the four
native syntaxes nor a section A.4 one, INVALID_ENCAPSULATED_PIXEL_DATA
when it is a section A.4 one and the top-level Pixel Data is not a fragment
stream section A.4 allows, DIRECTORY_OFFSET_UNRESOLVED when a DICOMDIR
carries an offset that is not zero and names no Directory Record the
Dataset holds, or is not one 32-bit unsigned integer (never written stale
or as zero), or DIRECTORY_OFFSET_DEFLATED when a DICOMDIR under Deflated
Explicit VR LE carries a non-zero offset. Nothing is returned on a throw.
Example
import { parseDicom, serializeDicom } from "@cosyte/dicom";
const ds = parseDicom(buf);
const out = serializeDicom(ds); // spec-clean Part 10, same transfer syntax
// parseDicom(out) re-reads to a structurally-equal dataset.
toDate()
toDate(
value,options?):Date|undefined
The value as an absolute instant, ONLY where the zone is determinate.
A stated &ZZXX offset wins and options.assumeOffsetMinutes is ignored.
With no stated offset the caller's assumeOffsetMinutes is applied, an
explicit 0 meaning "treat this naive value as UTC". With neither, the
answer is undefined: the host machine's zone is never read and UTC is never
assumed. A value with no year is never an instant, so a TM always answers
undefined however the call is made. A non-finite assumeOffsetMinutes
(NaN, Infinity, -Infinity) names no zone either, so it answers
undefined rather than an Invalid Date whose getTime() is NaN, and so
does a finite offset so large that applying it leaves the range a Date can
represent. A value naming no real calendar point is refused before any of
that, so an impossible day is never rolled into the next month.
Components below the stated precision fill to their lowest legal value for
the instant alone; the value's own precision is untouched, and a later
toObject or toISO on it returns what it returned before. A
four-digit year below 100 stays that year, so 0050 is the year 50 and the
two-digit remapping of Date.UTC never reaches the result.
Parameters
value
DicomTemporal | null | undefined
options?
Returns
Date | undefined
Example
import { parseDate, parseDateTime, toDate } from "@cosyte/dicom";
toDate(parseDate("20240115").value); // undefined: no zone was stated
toDate(parseDate("20240115").value, { assumeOffsetMinutes: 0 })?.toISOString();
// "2024-01-15T00:00:00.000Z"
toDate(parseDateTime("20240115133015+0100").value)?.toISOString();
// "2024-01-15T12:30:15.000Z"
toDate(parseDate("18700230").value, { assumeOffsetMinutes: 0 });
// undefined: never 1 March for a value the sender wrote as February
toISO()
toISO(
value):string|undefined
The value as an ISO-8601 string, TRUNCATED to the precision it stated.
Nothing is padded out: a DA renders 2024-01-15, a DT that stated only
an hour renders 2024-01-15T13, and a TM renders the bare time. Fractional
digits are rendered exactly as written. A stated offset is appended, Z when
it is zero; a value that stated NO offset gets nothing appended, because a
fabricated Z would claim UTC the sender never wrote.
Returns undefined for an invalid value, for null / undefined, for a
value that stated no component at all, and for one naming no real calendar
point: a string such as "2024-02-30" is worse than no answer, because every
ISO-8601 reader moves it silently to 1 March. It never throws. This is not a
byte round-trip of the wire value and is not meant to be: serializeDicom
remains the route that reproduces the original bytes.
Parameters
value
DicomTemporal | null | undefined
Returns
string | undefined
Example
import { parseDate, parseDateTime, parseTime, toISO } from "@cosyte/dicom";
toISO(parseDateTime("20240115133015-0500").value); // "2024-01-15T13:30:15-05:00"
toISO(parseTime("133015").value); // "13:30:15"
toISO(parseDate("18700431").value); // undefined: April has no 31st
toObject()
toObject(
value):DateParts|undefined
The calendar components a DA, TM or DT value stated, as a frozen object.
Returns undefined for a value the decoders marked valid: false, for
null / undefined, for a value that stated no component at all, and for
one whose components name no real calendar point (30 February, 29 February
outside a leap year, 31 April, second 60). It never throws.
hours / minutes / seconds are renamed to the singular
hour / minute / second; raw, valid and the legacy and
nonstandardOffset flags the decoders report beside the value never appear.
Parameters
value
DicomTemporal | null | undefined
Returns
DateParts | undefined
Example
import { parseDate, parseDateTime, parseTime, toObject } from "@cosyte/dicom";
toObject(parseDateTime("20240115133015").value);
// { year: 2024, month: 1, day: 15, hour: 13, minute: 30, second: 15 }
toObject(parseTime("133015.123456").value).millisecond; // 123, from the digits
toObject(parseDate("18700230").value); // undefined: February has no 30th