Troubleshooting & known limitations
@cosyte/dicom is built to be correct and honest about its edges rather than to claim more than
it delivers. Mis-reading a patient identifier, an image's signedness, or a rescale slope can cause
real clinical harm, so this page is the deliberate "do not over-trust" list: the error model, common
symptoms, and (critically) the explicit metadata-first boundary and the list of what is not
in scope. Everything here is a documented boundary, not a bug: the lenient parser never silently drops
or garbles data; where a limitation applies, the raw bytes are preserved (often with a warning), they
are simply not further interpreted.
When does it throw vs warn?
Only four unrecoverable Tier-3 conditions throw a DicomParseError; everything else is a warning
on ds.warnings.
import { parseDicom } from "@cosyte/dicom";
// Bytes that are not a Part 10 object: a structural fatal, not a tolerated quirk.
parseDicom(Buffer.from("plainly not a DICOM object, just ASCII bytes", "ascii"));
// throws DicomParseError (NOT_DICOM_PART_10)
| Fatal code (throws) | Meaning |
|---|---|
NOT_DICOM_PART_10 | No preamble/DICM and no recoverable File Meta: not a Part 10 object. |
INVALID_FILE_META | The File Meta group is present but structurally unreadable. |
UNSUPPORTED_TRANSFER_SYNTAX | The transfer syntax UID is not one of the four v1 syntaxes. |
EMPTY_INPUT | Zero-length input. |
Narrow on the caught error via err instanceof DicomParseError and err.code === FATAL_CODES.* (see
Tolerance & the warning model). Everything a real-world archive does short
of that (a missing preamble, an odd-length value, an off-spec VR, a group-length mismatch) is a
warning you triage, not an exception you catch.
Common symptoms
| Symptom | Likely cause | What to do |
|---|---|---|
ds.get("PatientName") is undefined | get takes the tag form, not a keyword | Use the tag (ds.get("00100010")), or resolve a keyword with Dictionary.byKeyword("PatientName")?.tag. |
ds.image.rescaleSlope is undefined | Rescale Slope was absent | This is by design. It is not defaulted to 1. Apply a fallback deliberately in your own code if the modality warrants it. |
ds.image.signed is undefined | Pixel Representation (0028,0103) was absent | Signedness is unknown, never guessed. Do not assume unsigned. |
A DICOM_VR_MISMATCH warning | The on-wire Explicit VR disagreed with the dictionary | The on-wire VR is used (Postel's Law: on the read path the sender's own declaration wins) and the deviation recorded; check the sender's encoding. |
A DICOM_PRIVATE_CREATOR_UNKNOWN warning | A private tag's creator is not in the active profile | The element degrades to UN; add the creator via a profile to resolve it. |
ds.get(tag)?.value is { kind: "binary" } for Pixel Data | Pixel data is exposed raw, never decoded | Expected. Decoding pixels is out of scope (see below). |
A DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED warning after deidentify | The object may carry burned-in PHI in the pixels | Metadata de-id cannot clean pixels; route to a pixel-cleaning step before sharing. |
A DICOM_SQ_NOT_DESCENDED warning | A defined-length Implicit VR LE element resolved to SQ from the dictionary, but its value is not an (FFFE,E000) item stream | The bytes are kept intact on Element.rawBytes and the rest of the object parses, but Element.items is absent, so nothing can navigate inside it. deidentify() therefore empties that element rather than shipping bytes it cannot audit, unless RetainSafePrivate plus a profile vouched for it - see the next row. |
A DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE warning after deidentify, and an attribute you expected is now empty | An element reached deidentify() whose on-wire VR is not one of the 34 PS3.5 §6.2 defines, so no Table E.1-1 row can say what its bytes mean | Fail-safe by design. Two very different files reach it: one conformant to a future edition of PS3.5, carrying a VR this release does not know (the header is read correctly per §6.2 and the value is emptied anyway, because nothing can classify it); and one malformed upstream, where an under-declared Value Length left the reader mid-value and leftover bytes tiled into a Data Element header. report.undefinedVrElements names the byte offset and the byte length dropped - deliberately not a tag, because on the second route a fabricated header's tag bytes are part of some element's value. Capped at 64 entries; the emptying is not. An Implicit VR LE file cannot trip it. |
A DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE warning after deidentify, and a sequence you expected is now empty | That SQ element reached deidentify() with no items, so its item stream could not be walked | Fail-safe by design: PS3.15 §E.1.1 obliges a de-identifier to reach listed attributes inside a Sequence of Items, and a run that cannot enumerate them must not pass them through. report.unauditableSequences names the tag and the byte length dropped (capped at 64; the emptying is not). Usually a sender-side encoding defect, but not always: a conformant file nested past this library's own NESTING_DEPTH_LIMIT of 64 is refused identically. |
A DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED warning after deidentify, and a value you expected is now empty | The sender over-declared that element's Value Length, so the element that followed it was absorbed into its value | The carrier is emptied rather than kept, because an attribute encoded inside a value is invisible to the PS3.15 action table. report.embeddedAttributes names the carrier and the tags that were hiding in it. The file is malformed at source - raise it with the sender. |
A DICOM_ITEM_CROSSES_SEQUENCE_END warning | An (FFFE,E000) Item declared a length reaching past the end its enclosing SQ declared, so the item's value read takes bytes that belong to the enclosing Data Set | The sender's two length fields contradict each other: PS3.5 section 7.5.2 makes the SQ's Value Length the exact extent of the item stream, section 7.5.1 governs the Item's own length, and this file gets one of them wrong. The reading is unchanged and this is disclosure only - the parser follows the Item's length, as every release has, so nothing about how your file is read changed in this release. Where the disputed element lands is shape-specific, and this warning cannot tell you which shape you have - the two readings are byte-identical. Both are pinned as measurements rather than summarised: on one, the element that follows the sequence becomes a per-item attribute under a deidentify() contextPath naming an item it was never in; on the other, it stays at the root with no contextPath at all. Treat a de-identified object built from such a file as unverified, and do not trust (0012,0062) Patient Identity Removed = YES on it. The private-attribute routes this used to fire beside are closed (DICOM-PRIVATE-CREATOR-RESERVATION-LEAK for the absorb direction, DICOM-ITEM-EJECT-ROUTE for the eject direction), but that is a statement about private attributes under RetainSafePrivate, not about the object - the structural relocation this warning is about is unchanged, and a private SQ a profile vouches for is still kept verbatim and never walked. Which way an element goes depends on where the sender put it relative to the disputed sequence, and this warning does not report that - so read it as "the structure is in dispute", never as an all-clear about what was retained. The parser deliberately does not prefer the sequence's extent, even though 7.5.2 reads like it should: a file whose item over-declares and a file whose sequence under-declares are byte-identical, so no reader can honour 7.5.2 on the first without imposing it on the second. Check Element.items against what you expected before trusting a contextPath, and raise the file with the sender. |
A DICOM_DUPLICATE_TAG_IN_DATA_SET warning | The same Data Element Tag was read twice in one Data Set, so the second element REPLACED the first | This one reports a loss, not a tolerated deviation. A parsed Data Set is a Map<Tag, Element>, so the earlier element's value is not in the object and cannot be recovered from it; the survivor is indistinguishable from an element the sender wrote once, which is why the parse says so explicitly. The warning's position.byteOffset is the offset of the header that replaced, which is the surviving Element.byteOffset. That makes it a lookup for a collision at the ROOT and nowhere else: inside a defined-length Sequence Item, Element.byteOffset is relative to that item's own slice, so the same number can also name an untouched root element, and nothing on the warning says which Data Set it came from (position.contextPath is not populated by any parser warning). Read it as "an element was destroyed somewhere in this object" unless you have established the frame. PS3.5 section 7.1 requires a tag to occur at most once in a Data Set and section 7.5.1 requires the same inside an Item, so a conformant file cannot raise it. The usual cause is not a sender writing a tag twice: it is a length field that lies, so bytes inside some element's value are read as a Data Element header, or an Item that under-declares and ejects its trailing elements into the enclosing Data Set (see the row above). The reading is unchanged and this is disclosure only - the last element read still wins, exactly as in every previous release, and no value is invented for the one it replaced. The message names no tag, deliberately: on the length-lie route those four bytes are document content. Treat the object as incomplete, do not trust a deidentify() report built from it, and raise the file with the sender. |
A DICOM_DUPLICATE_FILE_META_ELEMENT warning | The same (0002,xxxx) tag was read twice in the File Meta group, and it is one of the eight this library projects into typed FileMeta fields | The same loss as the row above, in the group that decides how every following byte is read. The File Meta group is an array, not a map, so nothing is overwritten - but a modeled tag is answered by a first-match search and is excluded from FileMeta.extraElements, so a second copy is in neither the typed field nor the verbatim residue. It left the object. The two codes resolve a repeat the opposite way round, deliberately, because the two readings do: the FIRST copy wins here, the LAST read wins in a Data Set. Neither reading changed in this release, and no value is guessed for the copy that lost. Two copies of (0002,0010) Transfer Syntax UID carrying different UIDs are two readings of the same file and the order alone picks one - a file like that may parse cleanly, parse into a different object, or throw INVALID_FILE_META from the dataset parser, depending only on which copy came first. Unlike the Data Set code, position.byteOffset is unambiguously file-absolute (the File Meta group is never nested) and locates the copy that was dropped, not the survivor. A repeated (0002,xxxx) this library does not model is silent, because every copy of one is kept in extraElements - and serializeDicom then re-emits both copies, PRE-EXISTING and unchanged here. It covers the group as the parser delimits it, not the group: a copy an intermediary appended past an honest (0002,0000) group length is never a File Meta element to this parser and is relocated into the main Data Set, silently, on this release and every earlier one. PS3.5 2026c section 7.1 requires a Data Element to occur at most once in a Data Set; PS3.10, which governs this group, is not vendored in this repo, so no conformance verdict is claimed here - the code's trigger is narrower and does not need one, firing exactly when a value the file carried does not reach the parsed object. The message names no tag. Raise the file with the sender, and do not trust a deidentify() report built from it. |
A DICOM_DEIDENT_METHOD_NOT_ADDED warning after deidentify, and the prior (0012,0063) text is gone | The (0012,0063) value deidentify() would have had to write - the one the file already carried, with or without this run's method text appended to it - would not fit the largest Value Length an LO can encode | PS3.15 section E.1.1 says a de-identifier's method text is "inserted in or added to" that attribute, so deidentify() adds. This code is raised when the value it would have to write is longer than that VR can encode: LO is a short-form VR with a 16-bit Value Length, and an element the serializer cannot encode would take the whole de-identified object down rather than lose one attribute. The prior value is replaced instead, which is what every release before this one did on every file, and this warning exists so that fallback is disclosed. Read it as "the length ceiling was reached", never as "every fallback is disclosed": a (0012,0063) a file encoded under a VR other than LO is also replaced, and that one is disclosed by its own code, DICOM_DEIDENT_METHOD_NOT_LO, and is a different fallback with a different cause. Truncating your earlier de-identification records instead was refused deliberately: choosing which to drop is a policy the standard does not state. Reachable only from a (0012,0063) already at or within a few bytes of 65,534 - including one that needs no append at all, because it already records this run's method. The message carries no value and no length. If you need the full chain, read (0012,0063) off the SOURCE dataset before de-identifying. |
A DICOM_DEIDENT_METHOD_NOT_LO warning after deidentify, and the prior (0012,0063) text is gone | The source file encoded (0012,0063) under a Value Representation other than LO, so deidentify() replaced it rather than adding to it | The join that makes (0012,0063) a provenance chain concatenates LO values with the 5CH delimiter, which is a text operation defined only for the VR the Data Dictionary gives that tag. Bytes under any other VR are not values it can join into (an OB or UN value is arbitrary octets), so guessing an encoding for them was refused: this library reports rather than invents. This is the same loss as DICOM_DEIDENT_METHOD_NOT_ADDED for an unrelated reason, which is why it is a separate code: one says the chain outgrew the VR, this one says the bytes were never in that VR to begin with. Every release before this one made the same replacement silently. A prior value that is empty or padding only raises nothing, because nothing was lost. The warning carries no value, no length and no VR: two bytes read out of a fabricated header are document content, not structure. |
A DICOM_DEIDENT_METHOD_PRIOR_RETAINED warning after deidentify | The source file already carried a (0012,0063) De-identification Method value, and it is in the de-identified output beside the one this run recorded | PS3.15 section E.1.1 says a de-identifier's method text is "inserted in or added to" that attribute, so keeping the sender's earlier record is the conformant act and this warning is not a defect report - it is a disclosure. (0012,0063) is not in Table E.1-1, so no rule in the run inspected, audited or redacted those bytes: if the sender wrote identifying text there, that text is in output stamped (0012,0062) Patient Identity Removed = YES. The warning carries no value, no length and no VR; position.byteOffset locates the element. It is not recorded on report.retained, which lists the Annex E option sets active for the run. Read it as "bytes from the input file are in (0012,0063)", never as "the sender wrote something identifying": de-identifying an object this library already de-identified raises it too, because the prior value is then this library's own earlier record and nothing on the wire tells the two apart. If your source may put identifying text in (0012,0063), read the attribute off the de-identified dataset and decide for yourself - this library will not remove an attribute no profile lists. |
Keeping PHI out of logs
A Tier-2 warning is safe to log whole. Its message comes from a frozen registry keyed by the
code, and the only things substituted into it are structural: a tag, a VR checked against the closed
34-VR set, and input-derived numbers. No warning factory accepts a string read out of the document,
so ds.warnings holds codes, positions and registry prose, not patient data.
Two things that array does not cover:
- A
DicomParseErrorcarries asnippet: up to 16 bytes of the source as hex. Those are raw input bytes and the library does not redact them. Logerr.code,err.byteOffsetanderr.message; treaterr.snippetas PHI.{ strict: true }turns every Tier-2 warning into one of these, so the frozen-registry guarantee above covers themessageand nothing else on the strict path. Which element the bytes belong to is not contracted: that offset's frame follows where the element was read (file-absolute at the root, relative to the enclosing slice inside a defined-length Sequence or Item, into the inflated stream under Deflated Explicit VR LE), while the snippet is cut from whichever buffer the parse is holding, so the two can disagree and the bytes can be an unrelated element's value. Do not reason from a code's message to what its snippet holds. Review the two paths separately; a message-only review of the lenient path does not transfer. - Value-decode deviations do not appear on
ds.warnings. Decode is lazy, so aDAin a legacy format or aUIwith the wrong pad surfaces on the decoded value's ownwarnings(el.value.warnings), never folded into the frozen dataset array. Those messages are built from the same registry and are equally safe, but a logger that only readsds.warningswill not see them at all.
A DeidentifyReport is safe to log with two exceptions, not one. uidMap's keys are the source
UIDs read out of the file: they are there so UID replacement stays consistent across a study, and a
study UID is a unique identifier. And removedPrivateTags is composed from tag numbers, which are
structural on every conformant file but are read out of the document, so a fabricated odd-group
header makes four bytes of somebody's value into a tag string there. The rest of the report (keywords,
action codes, sequence context paths) is composed from static tables and carries nothing.
The field-by-field split between identifiers and values is in
Tolerance.
Keep the same discipline in your own code: log w.code and w.position, not the element value.
What's not yet parsed, and what is out of scope
Depth tracks the code and never leads it. These are the deliberate boundaries, authored here so a reader never relies on something absent.
The metadata-first boundary (scope, by design)
@cosyte/dicom reads and writes DICOM metadata. These are permanent non-goals for this package.
Each is tracked as a future companion package, not a gap to be filled here:
- No pixel decoding. Pixel Data is exposed as a raw
Buffer(and, for encapsulated transfer syntaxes, its fragments) and is never decoded, decompressed, windowed, rescaled, or color-transformed. Rescale Slope/Intercept, Window Center/Width, and the LUT sequences are surfaced as metadata, but applying them to produce displayable pixels is deferred to@cosyte/dicom-pixel. - No DIMSE networking. There is no C-STORE / C-FIND / C-MOVE / C-ECHO, no SCU/SCP, no association
negotiation. This is a file/buffer library, not a PACS node. That is
@cosyte/dicom-net. - No DICOMweb. No QIDO-RS / WADO-RS / STOW-RS client or server: deferred to
@cosyte/dicomweb. - No pixel-level de-identification.
deidentifycleans metadata per PS3.15 Annex E; burned-in annotation is warned, never removed (DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED). Pixel scrubbing belongs to@cosyte/dicom-pixel.
Boundaries within the metadata surface
-
Four v1 transfer syntaxes. Implicit VR LE, Explicit VR LE, Explicit VR BE, and Deflated Explicit VR LE are read and written. A compressed pixel stream inside those syntaxes is passed through byte-for-byte, never decompressed.
-
Only typed
FileMetafields round-trip.serializeDicomrecomputes a spec-clean File Meta group; File Meta elements outside the typed model are not preserved verbatim through the model. -
De-identification is metadata-only and fail-safe toward removal. Conditional Annex E codes collapse to their most-protective branch (no IOD Type-1 analysis); private attributes are removed by default unless a profile marks a creator's tags safe.
-
An element that over-declares its own Value Length hides the next element inside its value, and
deidentify()empties rather than keeps it. PS3.5 defines Value Length as the length of that element's Value Field; a sender that writes a larger number produces a file whose reading is self-consistent and in which the following element has been absorbed, header and all. Nothing on the wire says which length lied, soparseDicomreads it exactly as written and this is not recoverable at parse time. Whatdeidentify()does is narrower and fail-safe: before keeping a value, it checks whether the value's tail decodes - in the file's own transfer syntax - as whole Data Elements ending exactly at the end of the value, at least one of which this run would have acted on, and containing a byte the carrier's VR cannot legally hold (PS3.5 §6.1.3 and Table 6.1-1 permit five control characters in DICOM text; Table 6.2-1 decides which of them each VR may hold - all five inLT/ST/UT, ESC only inLO/SH/UC/PN, none elsewhere). If so the value is emptied,report.embeddedAttributesrecords the carrier and the tags found inside it, andDICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVEDis raised. Carriers are string VRs only, and the gap is live rather than theoretical: the identical over-declare into anOB,OW,UNorUScarrier still writes the identifier into de-identified output with no warning and no report entry. Arbitrary bytes are what those VRs are for, so no content test can tell a swallow from a legitimate value there. Treat a file that raised this warning as a file whose sender is malformed: other attributes in it may be carrying the same defect where it cannot be seen. -
An unrecognized Explicit VR is read long-form, and
deidentify()still empties the element. Two separate rules, one clause. PS3.5 2026c §6.2 says every VR defined in a future edition "shall be of the same Data Element Structure as defined in [§7.1.2] with reserved bytes after the VR and a 32-bit unsigned integer VL". So when the two on-wire VR bytes are not one of the 34 this release knows,parseDicomreads a 12-byte header, exactly as it does forOBorUT- andserializeDicomwrites one. Before0.0.9it read an 8-byte header, which took the length from the two bytes §6.2 reserves. What such a file did then depended on its own payload bytes - sometimes a whole-object refusal, sometimes a clean parse into a tree the sender did not write - so there is deliberately no one-sentence account of it here;scripts/measure-unrecognized-vr.tsin the repository prints the per-shape table for both readings. The value is otherwise treated like any other: a declared length past the end of the buffer, or an undefined length, is refused the same way it is for every non-SQVR. The de-identify rule is unchanged and still fires, because reading the header is not the same as knowing what the value means: Table E.1-1 acts per attribute, and nothing can say whether a VR from a later edition holds a name, a date or an opaque blob. Sodeidentify()empties the element,report.undefinedVrElementsrecords it, andDICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLEis raised. The record is capped at 64 entries; the emptying is not. On a conformant future-VR file that is a real cost - a legitimate value is destroyed because this release cannot classify it - and it is the same over-redaction trade the sequence rule below makes. The finding names a byte offset and no tag, uniquely among the report's findings, and that is deliberate. The header may still have been fabricated out of the middle of some element's value: bytes that happen to form a complete long-form header tile just as readily as short-form ones did, and on a carrier whose payload is"MR BRAIN SMITHSO"the fabricated tag renders as48544F53, four letters of the surname. An unrecognized VR written honestly raises the same code with an ordinary tag, and nothing here can tell the two apart, so the tag is withheld on both routes rather than on a guess. The byte offset locates the element instead and is a position the parser counted. An Implicit VR LE file cannot reach any of this: there the VR comes from the dictionary.UNis one of the 34 and is unaffected. There is no exemption: unlike the sequence rule below,RetainSafePrivateplus aProfiledoes not keep such an element. If the element previously appeared inreport.embeddedAttributes(an undefined-VR carrier whose bytes happened to tile as Data Elements), it now appears inreport.undefinedVrElementsinstead. -
A sequence
deidentify()could not open is emptied, not kept, so expect data loss on a malformed file. Recursion is driven byElement.items. AnSQelement that has none is un-auditable: PS3.5 §7.5.1 says its value is "a DICOM Data Set composed of Data Elements", and PS3.15 §E.1.1 obliges an implementation claiming the Basic Profile to protect the listed attributes "whether contained in the top level Data Set or embedded in an Item of a Sequence of Items". A run that cannot enumerate them cannot discharge that, so it discharges it on the carrier: the element is replaced with a zero-item sequence,report.unauditableSequencesrecords the tag and the byte length dropped, andDICOM_DEIDENT_SEQUENCE_NOT_AUDITABLEis raised. That record is capped at 64 entries so a crafted file cannot amplify a per-element diagnostic; the emptying itself is never capped, so an array exactly 64 long means "at least 64". Before this, those bytes were written into de-identified output verbatim, identifiers and all, with a clean report. The accompanyingDICOM_SQ_NOT_DESCENDEDonds.warningssays why the parse refused, and that is usually a sender-side encoding defect worth raising with them. It is not always the sender: a conformant file whose sequences nest deeper than this library's ownNESTING_DEPTH_LIMITof 64 is refused the same way and loses that sequence too. Two shapes are exempt from the rule and still leak. A privateSQkept underRetainSafePrivateplus aProfileis kept verbatim and unaudited: the profile vouched for the element, and this rule runs after that decision. And an undefined-lengthUNvalue the CP-246 descent could not read as a sequence keepsvr === "UN"and raises nothing beyond a possibleDICOM_VR_MISMATCH. The rule cannot be extended to that one, because every ordinaryUNelement also hasitems === undefinedand applying it there would empty every unknown-VR element in every file. So for either, the reliable test is stillel.items === undefined, and a report is a record of what was reached, not a proof that everything was. -
A private value vanishes under
RetainSafePrivate, andreport.removedPrivateTagsnames it. The file's own length fields contradict each other about where a Sequence Item ends: its(FFFE,E000)Item declares more bytes than the enclosingSQelement's Value Length allows, so the Item reads past the end of its sequence and absorbs the element that followed it. PS3.5 2026c §7.8.1 scopes a Private Creator's block reservation to the Data Set it appears in and an Item is its own Data Set ("The scope of the reservation is just within the Item. Items do not inherit the Private Data Element reservations made by Private Creator Data Elements in the Data Set in which the Item is nested"), so on such a file which Data Set a private element is in is not determined by the file. PS3.15 2026c §E.3.10 retains only Private Attributes "known by the de-identifier to be safe from identity leakage" and requires that "all other Private Attributes shall be removed or processed in the element-specific manner recommended by Deidentification Action (0008,0307), if present within Private Data Element Characteristics Sequence (0008,0300)". That clause licenses two dispositions; this library does not implement(0008,0307), sodeidentify()removes every private element it reaches in that Item and at every depth below it (a privateSQthe profile vouches for is settled before the descent and is kept verbatim, so nothing inside it is examined), and records each inreport.removedPrivateTags. This is the fix for a leak measured on the published0.0.8tarball and needing the opt-inRetainSafePrivateplus a vendor profile: a private value the sender wrote outside the sequence was retained on the Item's reservation,report.removedPrivateTagsread[], and the object was still stamped(0012,0062) Patient Identity Removed = YES. The mirror direction is also refused, sinceDICOM-ITEM-EJECT-ROUTE. An Item that under-declares ejects its trailing elements out into the enclosing Data Set, where a Private Creator that lands there would otherwise vouch for elements the sender never reserved. So a Data Set retains nothing private that it holds after a sequence whose own contents contradict the extent it declared, at every depth and not only at the root. A reservation the sender wrote ahead of that sequence is untouched. Two shapes of the same contradiction are covered, because the parser records them differently: an item stream that over-runs the sequence under Explicit VR, and a sequence the parser could not walk at all (DICOM_SQ_NOT_DESCENDED) under Implicit VR LE. A Data Set is aMap<Tag, Element>, so an ejected element whose tag it already holds overwrites in place; the rule checksElement.byteOffsetas well as position for that reason. That overwrite destroys the Data Set's own element beforedeidentify()ever runs, silently and with no report entry, and this release does not change that: if you need the original, the fix is at the sender. One shape is still not covered and still leaks,PRE-EXISTINGand pinned by a test: a privateSQa profile vouches for is kept verbatim before the descent runs, so nothing inside it is examined. For it,report.removedPrivateTagsreading[]is not proof that nothing was retained.ds.warningsis not the audit channel for any of these shapes. Where aDICOM_ITEM_CROSSES_SEQUENCE_ENDwarning does fire it means the file's structure is in dispute, so it never says the retention was audited and is never an all-clear about what was retained. If you expected those private attributes and need them, the fix is at the sender: an Item's declared length and its sequence's declared length have to agree. It costs content on files that lie: where the creator and the data element were both genuine Item content the reservation was real, and it is removed anyway, because nothing on the wire distinguishes that case. A file whose length fields agree is unaffected, whatever they say. -
Repeating-group rows are matched by mask, within the range the standard bounds them to.
(50xx,xxxx)Curve Data,(60xx,3000)Overlay Data and(60xx,4000)Overlay Comments are stated by the standard as a group mask rather than a single tag.deidentify()matches them in the sixteen even groups PS3.5 defines (6000-601Efor overlays,5000-501Efor curves), removes them, and records them in the report with arepeatingGroupfield naming the mask that matched. Even groups above the bound and odd groups are not overlay or curve groups and are left alone; odd groups are private and go through the private-attribute path instead. -
RetainLongitudinalTemporalmeans the standard's full-dates option, the less protective one. PS3.15 defines two longitudinal-temporal options, full dates and modified dates. This package exposes one name for both and it carries the full-dates column, so on the 169 attributes where the two columns disagree you keep the real value where modified-dates would have cleaned it. Activate it only when real dates are genuinely required; date shifting is not done at this layer.
Scope (non-goals)
- A parser + serializer + de-identifier for DICOM Part 10, metadata-first. Not a viewer, not a network stack, not a pixel toolkit.
- Pre-alpha, published on npm. The package is public on npm (
npm install @cosyte/dicom) but sits on the0.0.x-until-first-alpha ladder: pin an exact version, and expect the surface to keep moving until first alpha. For the current version, readnpm view @cosyte/dicom versionrather than a number written in a doc.
For the full public surface and the exact fields each view decodes, see the package's README.md and
the Core Concepts.