Skip to main content
Version: v0.1.0

Known limitations: the "do not over-trust" list

Read this page before you put @cosyte/dicom in front of real data.

Mis-reading a patient identifier, an image's signedness, or a rescale slope can cause real clinical harm, and a de-identifier that reports a clean run it did not perform is worse than one that refuses. This package is engineered to prevent over-trust, so the boundary is a deliverable, not a footnote: it is a page of its own, held to the same bar as the rest of these docs, and the introduction and the cookbook link into it from every route that depends on it.

This page is an index, not a census. Each entry is one line plus a link to the section that owns it, and the owning section is where the measurements, the fixtures and the residuals live. Read the section before concluding a class is closed. Nothing here is a count: counts in this repository have been corrected twice and then deleted.


1. Scope: what v1 does not do at all​

These are non-goals, not gaps. Each is a companion package or another tool's job.

Not in v1Where it goes
Pixel decode or decompression in any transfer syntax, frame assembly from fragments, offset-table interpretation, rendering, windowing, or any measurement computed from pixels@cosyte/dicom-pixel
Repairing a fragment stream on write. serializeDicom writes each PS3.5 2026c section A.4 syntax and refuses a malformed top-level Pixel Data with INVALID_ENCAPSULATED_PIXEL_DATAout of scope
Burned-in annotation removal / Clean Pixel Data. A de-identified output from this package is metadata-de-identified only, and it warns rather than claiming otherwise@cosyte/dicom-pixel
Networking. No DIMSE: no C-STORE, C-FIND, C-MOVE, MWL or MPPS@cosyte/dicom-net
Web services. No DICOMweb: no QIDO, WADO or STOW@cosyte/dicomweb
Transcoding. The serializer re-emits in the dataset's source transfer syntax onlyout of scope
Terminology resolution. Coded values are surfaced with their designator and canonical source but are never validated, looked up, or cross-mapped; SRT is not normalized to SCT@cosyte/terminology
Typed SR and RT models. SR content trees and RT objects are navigable as raw structure, with no clinical model over them. Deliberately flagged and deferred rather than half-builtout of scope for v1
Patient matching. Patient ID is surfaced with its issuer; the library never decides that two identifiers are the same personout of scope
A File-set view. A DICOMDIR's Directory Record tree is read and its offsets rewritten on write (below), but no DICOMDIR is built from files and nothing is removed from a File-setout of scope for v1

Supported transfer syntaxes: the four native ones, Implicit VR LE 1.2.840.10008.1.2, Explicit VR LE ...1.2.1, Deflated Explicit VR LE ...1.2.1.99 and Explicit VR BE ...1.2.2 (retired, legacy-only), plus every encapsulation syntax PS3.5 2026c section A.4 names: JPEG Baseline, Extended, Lossless and Lossless First-Order Prediction; RLE Lossless; JPEG-LS Lossless and Near-Lossless; JPEG 2000 and HTJ2K; MPEG2; MPEG-4 AVC/H.264; HEVC/H.265 Main and Main 10; JPEG XL; Deflated Image Frame Compression; and Encapsulated Uncompressed Explicit VR LE. Dictionary.uid() names each UID. An encapsulated object is read under Explicit VR LE rules, as section A.4 requires, so its metadata parses in full and readPixelDataFragments hands back its Basic Offset Table and fragments as raw bytes. Its pixels are never decoded. serializeDicom writes each of those syntaxes back under the object's own UID, a de-identified object included: the Data Set as Explicit VR LE, and the top-level Pixel Data as OB of undefined length, its Basic Offset Table and fragment Items copied byte for byte and in order, then a Sequence Delimitation Item. Nothing is transcoded, re-framed or re-encoded, and no offset table is rebuilt. The writer's limit sits with it: a top-level Data Set section A.4 does not allow is refused with INVALID_ENCAPSULATED_PIXEL_DATA, never repaired (see below). The four JPIP Referenced syntaxes are read too, for metadata only (next paragraph). Every other registered UID stays the fatal UNSUPPORTED_TRANSFER_SYNTAX, named by its PS3.6 registry name: the SMPTE ST 2110 syntaxes and every retired UID, the retired JPEG processes included; serializeDicom refuses the same UIDs with its own UNSUPPORTED_TRANSFER_SYNTAX. Deflated Explicit VR LE deflates the whole dataset stream rather than the pixels, and it is inflated on parse.

The four JPIP Referenced syntaxes are read for metadata, and only read. JPIP Referenced ...1.2.4.94 and JPIP HTJ2K Referenced ...1.2.4.204 (PS3.5 2026c sections A.6 and A.11) are read under Explicit VR LE rules, as those sections require. JPIP Referenced Deflate ...1.2.4.95 and JPIP HTJ2K Referenced Deflate ...1.2.4.205 (sections A.7 and A.12) are that same Data Set compressed per RFC 1951, so they are inflated first, under the decompression cap and the inflate fatals Deflated Explicit VR LE has. Such an object carries no Pixel Data: it references its pixels through Pixel Data Provider URL (0028,7FE0), which ds.get("00287FE0") returns as the UR element the file carries, its decoded value the URL text with only its trailing padding removed. The URL is never fetched, resolved or validated, and nothing in this package opens a connection to the host it names. The limits sit with that capability:

  • serializeDicom refuses all four with UNSUPPORTED_TRANSFER_SYNTAX and returns no bytes. A JPIP object is read, never written.
  • deidentify() refuses all four: it throws a DeidentifyError carrying the UNSUPPORTED_TRANSFER_SYNTAX code, whatever options you pass, and returns no dataset and no report. (0028,7FE0) has no PS3.15 Table E.1-1 row, so a de-identified copy would keep the URL by omission, and whether that URL can carry identity is an open question this package has not ruled on. The refusal keys on the File Meta Transfer Syntax alone: a non-JPIP object that carries (0028,7FE0) is de-identified like any other and keeps it (see De-identification).
  • Three misuses section A.6 forbids parse without a code, as the section A.4 ones above do: an object with no (0028,7FE0) (then ds.get("00287FE0") is undefined, never an empty string or a placeholder), one that carries a top-level Pixel Data anyway, and one whose Photometric Interpretation is outside the four Values section A.6 allows. No IOD or module validation is done.

Three limits sit with that capability. A fragment stream that ends before its Sequence Delimitation Item still parses, with DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED saying the fragment list may be short. Two misuses of an encapsulation syntax that PS3.5 2026c section A.4 forbids parse without a code: an object with no top-level Pixel Data, and one whose top-level Pixel Data is native, for which readPixelDataFragments returns undefined, as it does under Explicit VR LE. And the writer is stricter than the reader: serializeDicom throws a DicomSerializeError with code INVALID_ENCAPSULATED_PIXEL_DATA, and returns no bytes, for every top-level Data Set under a section A.4 syntax that the section does not allow, rather than repairing it. That is no top-level Pixel Data, native top-level Pixel Data, Float or Double Float Pixel Data (7FE0,0008) / (7FE0,0009), a fragment stream not ended by its Sequence Delimitation Item (so an object read with DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED is read and not written, since a delimiter appended to a stream that may be short would be a well-formed file missing frames), anything other than an Item of defined length in the stream, an Item of odd length, a fragment Item of length zero after the Basic Offset Table (an empty Basic Offset Table is fine), and a stream with no Basic Offset Table Item or no fragment Item after it. Its message is a fixed string. Pixel Data inside a Sequence Item, such as an Icon Image Sequence, is written as read and not checked.

DICOMDIR: the record tree is read, and its offsets are rewritten on write. For an object whose (0002,0002) is 1.2.840.10008.1.3.10 (Media Storage Directory Storage), ds.directory is a DicomDirectory: every Item of the Directory Record Sequence (0004,1220) as a DirectoryRecord with its (0004,1430) type, its Referenced File ID (0004,1500) components and the records its (0004,1420) offset names as lower-level, and the root entity (0004,1200) names. An offset resolves only when it is exactly where a record's (FFFE,E000) Item tag sits in the file, counted from the first byte of the File Preamble, which is how PS3.3's Basic Directory IOD defines these offsets (PS3.3 is not vendored here, so no clause is claimed for it); one that is not, a Value Length other than 4, and a record reached twice each raise a warning (DICOM_DIRECTORY_OFFSET_UNRESOLVED, DICOM_DIRECTORY_OFFSET_MALFORMED, DICOM_DIRECTORY_RECORD_REVISITED) and resolve nothing. serializeDicom writes (0004,1200), (0004,1202), (0004,1400) and (0004,1420) as the byte offsets of the records they named when the file was read, including after deidentify() has moved every record. The limits sit with it:

  • Referenced File IDs are kept verbatim: never de-identified, rewritten or re-pointed, by the reader, deidentify() or the writer. A path that carries identifying text carries it into the output.
  • The two File-set clauses of PS3.15 §E.1.1 are not discharged. No DICOMDIR is created from the de-identified files, and the non-de-identified DICOMDIR is not removed from its File-set; deidentify() raises DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED on every DICOMDIR run.
  • A DICOMDIR carrying an offset that names no record is refused on write, with DicomSerializeError code DIRECTORY_OFFSET_UNRESOLVED and no bytes returned: an offset that did not resolve on read, a value that is not one 32-bit unsigned integer, and a non-zero offset on a Dataset whose Directory Record Sequence was removed or emptied, or was never there. It is never written stale (it would name the wrong bytes) and never written as zero (that prunes the tree).
  • A Deflated DICOMDIR with records is refused on write, with DIRECTORY_OFFSET_DEFLATED: a position inside a deflated stream names no Item a reader can seek to, so any non-zero offset is refused, and parseDicom resolves no offset of such a file and warns DICOM_DIRECTORY_OFFSET_DEFLATED, because PS3.10 requires a DICOMDIR File to use Explicit VR Little Endian. One whose offsets are all zero is written.
  • Not checked: record keys against PS3.3's Directory Record definitions, (0004,1202) against the end of the root chain, the File-set Consistency Flag, and Private Record UIDs. The retired MRDR offset (0004,1504) is written as read and not rewritten. An Implicit VR LE or Explicit VR BE DICOMDIR is read and written without a warning, although PS3.10 makes Explicit VR LE the only conformant syntax for a DICOMDIR File.

2. Open PHI residuals: measured, disclosed, and NOT closed​

None of these is an all-clear, and a DeidentifyReport that reads clean does not by itself close any of them. Each is either a product decision this package has deliberately not made, or a structural fact about DICOM that no reader can resolve from the wire.

  • RetainSafePrivate now keeps only what this run could account for, and everything else private is REMOVED. Two routes in the package write a private value into de-identified output: a Profile you pass, and the file's own Private Data Element Characteristics Sequence (0008,0300), which needs no profile. Four classes of value take them: one the run walked as Data Elements and put through the Annex E action table (a private SQ whose items the parser materialized), a Private Creator (gggg,00EE) whose whole decoded value is a member of your profile's private dictionary, a zero-length value, which encodes no Data Set, and one the file itself declares non-identifying - a block whose Block Identifying Information Status (0008,0303) reads SAFE, or an element a MIXED block lists in Nonidentifying Private Elements (0008,0304). 🩺 The fourth class is the sender's word and not this run's finding, and that is the limitation rather than a feature note. The value is retained unexamined because the system that wrote the file asserted the block is safe; an Item that does not resolve keeps nothing and says so under DICOM_DEIDENT_PRIVATE_DECLARATION_NOT_RESOLVED, but a well-formed declaration is taken at its word. PS3.15 §E.3.10 offers exactly one mitigation and it is yours: if you do not trust the sender, do not pass RetainSafePrivate, and every private attribute is removed. Anything else a profile vouches for is removed, recorded per instance in report.unenumerablePrivateRemovals with applied: "removed" and reason: "unenumerable", named in report.removedPrivateTags, and warned under DICOM_DEIDENT_PRIVATE_CARRIER_NOT_AUDITABLE. The removal record is complete and never capped at any input size; only the warnings are bounded. 🛑 The cost is over-redaction and it is nearly all of the profile route: an ordinary vendor scalar under an ordinary string VR that the file's own declaration does not name safe is removed. Decoding a value under the VR your profile declares for it is not an enumeration of what the value encodes, and neither is the embedded-attribute scanner's silence: that scanner reads string carriers only and decodes tiles in the file's own encoding, so a nested Data Set written in another transfer syntax passed it untouched on a perfectly scannable LO carrier - measured, and the reason the predicate is what the run DID with the value rather than the VR or the scanner's reach. If you need those values back, that needs a content test separating a nested Data Set from a legitimate binary blob, which is an open product question and not a flag. Earlier releases kept this class verbatim under (0012,0062) = YES and merely disclosed it, which was a disclosure and not a fix. The surface it shipped through is pinned as a measured matrix in test/integration/deident-private-reservation.test.ts rather than described in prose, because the prose has been wrong twice. Detail: Known limitations in the README.

  • Two audit-contract changes came with that, and a consumer switching on either has to act. report.unauditableSequences no longer produces its retired kept outcome for a retained private value - its applied field was "emptied" | "kept" and is "emptied" alone now, so an entry there means content is not in your output, which is what it meant before that second outcome joined it - and DICOM_DEIDENT_PRIVATE_CARRIER_NOT_AUDITABLE changes meaning from "this value was shipped unexamined" to "this attribute was removed unexamined". No published warning code is renamed or retired, so narrowing on that code name keeps compiling: re-read what it now means rather than re-typing it. DICOM_DEIDENT_PRIVATE_DECLARATION_NOT_RESOLVED is added, raised per Item of (0008,0300) that does not resolve to a block of private Data Elements reserved in that Data Set, so a declaration you expected to keep something and that kept nothing is never silent. The removal record (report.unenumerablePrivateRemovals) is a new report surface, and it is the only one that can tell you an attribute went for being unenumerable rather than by the action table.

  • An over-declared Value Length into a binary carrier still leaks. A length that swallows the following element into an OB, OW, US or UN value is not detected, and a (0010,0020) Patient ID inside it reaches de-identified output with no warning and no report entry. That is measured and still exactly true for this route, which is every default deidentify() run. The only carrier that gets a diagnostic is one that is itself a private attribute reached through RetainSafePrivate plus a Profile (the bullet above). A carrier reached through the file's own (0008,0300) declaration gets none: the declaration is what makes that value known safe, so the run keeps it rather than judging it, which is the sender-trust cost stated in that bullet. What the profile-route diagnostic says is "this value was not enumerated, so the attribute was removed" or "this value was dropped" (emptied, where the profile declares it SQ), never "a swallow was detected here". Arbitrary bytes are exactly what those VRs are for, so no content test can decide it. String carriers are covered, because there the same bytes are provably outside the VR's repertoire. If you accept files from a sender you do not control, treat a binary attribute's declared length as untrusted.

  • (0012,0063) De-identification Method can carry a value longer than the VR permits, and this library is the likeliest writer of it. PS3.5 2026c Table 6.2-1 caps an LO at 64 characters per Value, and (0012,0063) is 1-n. Every object this package de-identified without a caller-supplied method string, in every release up to and including this one, carried a single Value longer than that, in the one attribute a strict receiver reads to decide whether the object was de-identified at all. The method text is multi-valued now, one Value for the Profile and one per active Option. Re-de-identifying an object written by an earlier release keeps the over-long Value, because a prior record is added to rather than rewritten, which is the conformant act (PS3.15 2026c §E.1.1: the method text is "inserted in or added to" the attribute). Your own deidentificationMethod is not bounded for you either: split it on \ yourself if a strict receiver is in your path.

  • contextPath on a DeidentifyReport finding is inert and is not structural. It is not a key you can look anything up with, it is corroborated by nothing else in the report, and each segment's tag half is read off the wire with nothing behind it, so a fabricated SQ header the reader was desynchronized onto is named there. It is not safe to log.

  • What a warning message may contain, as a mechanism rather than as a verdict. The verdict form of this bullet ("safe on a well-formed file, not unconditionally safe") was corrected twice, so it is deleted rather than given a third wording. The registry's {tag} slot renders only a tag PS3.6's element registry carries a literal row for, and <withheld> otherwise; {vr} renders only one of the 34 VRs PS3.5 2026c §6.2 defines; and a raw number a header carries is bound out of the factory signature where it is bound at all, because a declared Value Length has neither a shape nor a membership a renderer could test. w.code and w.position carry nothing from the document. And a raw number SHIFTED by a constant the reader can compute is that raw number, which is the eighth instance of this class: DICOM_ITEM_CROSSES_SEQUENCE_END printed the bytes that remained inside the sequence, and that count is the sequence's own 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 src/parser/warnings.ts. No count of the copies is quoted, here or there: this package deletes a count it has corrected twice rather than incrementing it. What that closed, measured on an ST carrying "MR BRAIN SMITHSON " whose Value Length under-declares: under Explicit VR LE the reader desynchronizes onto a fabricated header whose declared length is odd, and in an earlier release DICOM_ODD_LENGTH_VALUE_PADDED rendered four bytes of the name as the tag and four more as the decimal length - eight payload bytes in one message. DICOM_NONZERO_RESERVED_BYTES printed two more as decimals on six other deltas. Both are closed. What it costs, stated rather than minimised: a message about a private element, a Group Length (gggg,0000) or a repeating-group member such as (6000,3000) Overlay Data no longer names its tag on any file, well-formed or not. The element is still in the Data Set under that tag, and position.byteOffset locates the header. The channel still matters as much as the field. The fixtures that produce a desynchronized read mostly die before a Dataset exists, so what carries their messages is onWarning and the { strict: true } DicomParseError rather than a surviving ds.warnings - a fact about those fixtures and not a promise about the parser. Treat both channels the same way. Full treatment: Keeping PHI out of logs.

  • Two carriers this list once named as open are now bound, and neither bound is an all-clear. DICOM_PRIVATE_TAG_NO_CREATOR, DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR and DICOM_PRIVATE_CREATOR_UNKNOWN take no tag at all any more, because an odd group is the one class of tag no closed table this library holds can vouch for; the element is still in the object and position.byteOffset still locates it. report.embeddedAttributes[].hidden now lists only tags this run acted on that have a literal row in PS3.15 Table E.1-1 - 653 of them - rather than any four bytes a run tiled over. A repeating-group mask hit is excluded and that exclusion is the whole bound: (50xx,xxxx) Curve Data leaves the entire 16-bit element number free, so a mask match proves a rule exists without making the membership finite, and a draft that stopped at "an even group" was measured admitting 500C5241. Two consequences: hidden can be empty on a real finding, which does not mean nothing was hidden, and it is still uncapped. report.removedPrivateTags is deliberately unchanged: narrowing it would empty the field on every well-formed file, which is what it exists to record.

  • A standard attribute from a PS3.6 edition newer than this build is REMOVED, conformant or not. deidentify() removes a non-private attribute that neither this build's PS3.6 2026d registry nor Table E.1-1 carries, because nothing on the wire separates a later edition's attribute from one a sender invented. The cost is content on a conformant newer-edition object, with no switch to keep it in this release; the removal is recorded by byte offset and never by tag. It narrows the older gap and does not close it: an attribute PS3.6 registers and the pinned Table E.1-1 does not list is still kept as the source wrote it. Detail: Attributes neither table carries.

  • A de-identified DICOMDIR is NOT File-set conformant, and the run says so rather than implying otherwise. PS3.15 §E.1.1's group-0004 bullet removes (0004,xxxx) from everything that is not a DICOMDIR, and this package does that; for an object whose (0002,0002) is 1.2.840.10008.1.3.10 it honours the carve-out and keeps them. Its Directory Records are de-identified as Data Sets, like any other Sequence Item, so a PATIENT record's Patient's Name and Patient ID are handled by Table E.1-1, and serializeDicom rewrites each record offset to where its record lands. The two File-set clauses are not discharged: no DICOMDIR is created from the de-identified files it references, and no non-de-identified DICOMDIR is removed from the File-set, because this library has no view of a File-set. Every such run raises DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED, including one where the object carried no (0004,xxxx) element at all, because what was not discharged has nothing to do with what the object happened to carry. Referenced File IDs (0004,1500) are kept verbatim, so a File-set whose paths carry identifying text keeps it; the record keys are only what Table E.1-1 names. Build a de-identified File-set from the de-identified files, not from this output.

  • (0028,0303) = MODIFIED is written, and this library performs no date transformation. deidentify() writes (0028,0303) Longitudinal Temporal Information Modified on every run, in the three states PS3.15 2026c defines: REMOVED when no Retain Longitudinal Temporal Information Option was active, UNMODIFIED when RetainLongitudinalTemporal was, and MODIFIED when RetainLongitudinalTemporalModifiedDates was. §E.3.6 defines those last two as mutually exclusive Options, so a call naming both is rejected with a DeidentifyError.

    The limitation is what MODIFIED does not mean here. §E.3.6 has two halves: the object's dates "shall be modified", and "the manner of date modification shall be described in the Conformance Statement". This library delivers the half a library can: it resolves Table E.1-1's modified-dates column, which is the more protective of the two temporal columns wherever they differ, and it writes the declaration. It performs no date transformation at all, on either branch, and it states no Conformance Statement, because PS3.2 Annex N scopes one to a named product and version and a library is neither. So you perform the date transformation, and a MODIFIED on an object whose dates nobody shifted is a caller defect this package cannot detect. It is never silent about it: every run under that Option carries DICOM_DEIDENT_DATES_NOT_TRANSFORMED on report.warnings, naming the half of §E.3.6 the run did not discharge. Nothing here claims PS3.15 Annex E conformance for the Retain Longitudinal Temporal Information With Modified Dates Option, which §E.1.1 makes all-or-nothing.

    If you shift dates yourself after the call under RetainLongitudinalTemporal, the UNMODIFIED in your output is wrong for your object: use RetainLongitudinalTemporalModifiedDates instead or overwrite the attribute, and describe the manner of modification in your Conformance Statement, which §E.3.6 requires of anyone claiming that Option. The declaration is the top-level Data Set's own, which is where §E.2 and §E.3.6 put it: (0028,0303) has no row in Table E.1-1, so a copy the sender nested inside a Sequence Item is retained by omission like every other registered attribute the table does not list, and still says whatever that sender wrote. Read the Data Set's own (0028,0303), never a nested one.

  • A DeidentifyReport is not safe to log whole. The value-bearing fields are named on the DeidentifyReport type. Read the list on the type, never a count quoted anywhere (including here): the count read one, then two, then three, and was wrong each time. (0028,0303) is not on it either: like (0012,0062), it is a statement the object makes about itself, and the report's shape is unchanged by it.

  • A DicomParseError is not safe to log whole. It carries snippet, up to 16 raw source bytes as hex, and the library does not redact them. Log err.code, err.byteOffset, err.offsetFrame and err.message. { strict: true } turns every Tier-2 warning into one of these, so a PHI review of the lenient path does not transfer to the strict one.


3. Structural facts that no reader can resolve​

These are not defects and they will not be fixed, because the information needed to fix them is not on the wire.

  • An over-declaring element and a well-formed one are byte-identical. Intent is not encoded. Any remedy therefore lives at the de-identify boundary or is a warning, never a parser bound.
  • A Data Set is a Map<Tag, Element>, so a repeated tag loses a value. The last read wins and nothing is guessed for the one it replaced; DICOM_DUPLICATE_TAG_IN_DATA_SET reports the loss. The File Meta group loses a repeat the opposite way round, first-match wins, reported by DICOM_DUPLICATE_FILE_META_ELEMENT. Neither can fire on a conformant file.
  • The writer orders what it can walk, and only that. A tag repeated inside an Item is written twice, a Sequence that cannot be walked, is nested past NESTING_DEPTH_LIMIT or whose parsed items do not match its bytes and any UN-carried Sequence are written as read, an element whose own bytes do not show where a reader ends it is written after the ascending rest of its Data Set whatever its tag, and an element a lying length relocated stays where it was placed. See Serialization.
  • Element.byteOffset inside a sequence item disagrees with itself and always has: 0 inside a defined-length item (its own frame), file-absolute inside an undefined-length one. The same is true of a warning's position.byteOffset. There is no frame-of-reference contract on either, and neither is covered by DicomParseError.offsetFrame - that names the frame for a thrown fatal and for the { strict: true } escalation of a warning, and for nothing on the lenient path. Measure it rather than assuming one.
  • A failed CP-246 UN descent emits nothing. The honest test for a consumer is el.items === undefined, not ds.warnings.
  • The byte-for-byte File Meta round trip is scoped to parse-then-serialize, and deidentify() is outside it on purpose. A de-identified File Meta group describes this de-identifying application (PS3.15 §E.1.1), so every non-modeled (0002,xxxx) element the source carried is dropped and the Source AE Title and the source's implementation identity go with it. This is a deliberate fidelity loss, recorded on report.fileMetaElementsDropped and counted on report.fileMetaElementsDroppedCount, and it is not recoverable from the output. If you need the source group verbatim, read it off the parsed dataset before the call.
  • NESTING_DEPTH_LIMIT is this library's bound, not the standard's. A fully conformant file nested deeper than it is refused, and a sequence refused that way is emptied by deidentify() rather than shipped, so expect data loss on such a file.

4. What is deliberately never defaulted​

The dangerous DICOM failure is the confident, wrong image, so the typed views answer "absent" rather than substituting a plausible value. undefined means the object did not carry the attribute.

  • image.rescaleSlope / image.rescaleIntercept: undefined must not be read as 1 and 0.
  • image.signed: undefined when (0028,0103) Pixel Representation was absent, never false.
  • image.photometricInterpretation: never defaulted to MONOCHROME2.
  • image.pixelSpacing and image.imagerPixelSpacing: different measurements, never substituted for each other.
  • patient.id: a string the sending system chose. Not globally unique, and never matched for you.
  • A malformed DS or IS token decodes to null, never to NaN coerced to 0.

5. Where the detail lives​

TopicPage
The error model and symptom-by-symptom triageTroubleshooting
What a diagnostic carries and what it does notTolerance
Keeping PHI out of logsKeeping PHI out of logs
The safety-critical views and their fail-safe rulesSafety
Writing spec-clean bytes back out, and what the writer will not doSerialization
The de-identification surface and the scope limits on itDe-identification
Working recipes, each citing the PS3 clause it readsCookbook

Everything on this page is a documented boundary rather than a bug. Where a limitation applies, the raw bytes are preserved (usually with a warning); they are simply not interpreted further.