A Delphi library for reading and writing the medical-image format DICOM (Digital Imaging and Communications in Medicine). A file is parsed — driven by its transfer syntax — into a tag-ordered dataset that recurses through nested sequences and decomposes encapsulated Pixel Data into fragments; a data dictionary of some 5,300 standard elements, generated mechanically from the official PS3.6 DocBook source, supplies keywords and value representations; and a pixel pipeline folds bit extraction, rescale, windowing and photometric inversion into a single lookup table. A pure-Pascal JPEG Lossless (Process 14) decoder is registered as a plug-in codec. The previous implementation is preserved on the main_old branch.
The library follows the architecture shared by the de-facto standard implementations (DCMTK, fo-dicom, GDCM, dcm4che): the dataset is a sorted map from tags to elements, the parser is separated from the model, codecs are registered per transfer syntax, and the data dictionary is data rather than code.
| Unit | Contents |
|---|---|
LUX.DICOM.pas |
the facade TdcmFile and type aliases — applications need only this unit |
Core/LUX.DICOM.core.pas |
TdcmTag, EdcmError, CheckDCM |
Core/LUX.DICOM.VRs.pas |
the 34 value representations as a strategy table _VRInfo_ |
Core/LUX.DICOM.Syntax.pas |
TdcmTranSyn — transfer-syntax resolution with graceful degradation |
Core/LUX.DICOM.Charse.pas |
Specific Character Set decoding, including ISO 2022 IR 87 |
Dictio/ |
dictionary runtime plus the generated LUX.DICOM.Tags.pas / LUX.DICOM.UIDs.pas |
Model/LUX.DICOM.Datset.pas |
TdcmDataset and the element classes TdcmValue / TdcmSequence / TdcmFragments |
IO/ |
TdcmSource, the parser TdcmReader, the serializer TdcmWriter |
Codecs/ |
codec registry; JPEG common layer and the SOF3 lossless decoder |
Pixels/LUX.DICOM.Pixels.pas |
frame-wise lazy decoding and the LUT pipeline |
Stream/LUX.DICOM.Stream.FMX.pas |
the only unit that depends on FMX — TBitmap conversion |
Tools/ |
dcmTest, dcmDump, dcmPix, dcmCmp, dcmRT, DictGen (console) |
Design rules: values are kept as raw bytes together with the original VR name and value length, so unknown tags, unknown VRs and UN elements survive a read–write round trip unmodified (PS3.5 §6.2.2 forbids byte-swapping them [1]); the single exception type EdcmError is raised through the gate CheckDCM; in the default lenient mode, violations of the standard are recorded as Issues and parsing continues, while strict mode raises instead.
A tag is a pair of 16-bit group and element numbers, compared by the 32-bit key
never by reading the packed record as a little-endian 32-bit integer, which would transpose the halves. TdcmDataset maintains its element list in ascending key order — DICOM encodes datasets in that order, so parsing appends in O(1), out-of-order files fall back to binary-search insertion, and lookup is O(log n).
The transfer syntax read from (0002,0010) — File Meta itself is always Explicit VR Little Endian — determines the layout tag | VR? | length | value of every subsequent element. In explicit VR the width of the length field follows from the VR:
An unrecognized two-uppercase-letter VR name is read in long form and preserved as UN — every VR added to the standard since 2015 (OD, OL, OV, UC, UR, SV, UV) is long-form, and this rule is the forward-compatible default. Implicit VRs are resolved through the dictionary hook, so an implicit defined-length SQ is still recursed into.
A length of $FFFFFFFF is resolved by linear parsing — items (FFFE,E000) and delimiters (FFFE,E00D) / (FFFE,E0DD) are consumed in stream order, sequences recurse into child datasets, and undefined-length UN values are read as Implicit VR Little Endian per CP-246. (The previous implementation searched the whole stream for the delimiter pattern with Boyer–Moore, which misfires on nested sequences and on compressed pixel data, and froze for minutes on the sample files; the linear parser completes each of them in under 0.4 s.) Encapsulated Pixel Data is decomposed into the Basic Offset Table and its fragments.
Text VRs are decoded per (0008,0005). For ISO 2022 IR 87 the escape sequences designate JIS X 0208, whose two-byte codes are converted arithmetically to Shift-JIS and decoded via CP932; delimiter-driven state reset is applied only while a single-byte set is designated, since a kanji byte may coincide with a delimiter code (e.g. the first byte of 秋 is =). ISO_IR 100–166, ISO_IR 13 and ISO_IR 192 (UTF-8) are also handled.
The stored value is extracted from a 16-bit pattern by two shifts whose casts are essential in Delphi, because shl/shr promote to 32-bit Integer and shr is a logical shift:
which both discards overlay bits above HighBit and sign-extends correctly. Rescale and the linear window of PS3.3 C.11.2.1.2 [3] — with defense against absent or multi-valued Window Center/Width and against widths below 1, falling back to the measured min/max — are folded, together with the MONOCHROME1 inversion, into one 65,536-entry (or 256-entry) lookup table, so converting a frame is a single table-lookup pass.
Codecs/LUX.DICOM.Codecs.JPEG.Lossless.pas implements ITU-T T.81 SOF3 [4] in pure Pascal and registers itself for the transfer syntaxes 1.2.840.10008.1.2.4.57 and .70. Canonical Huffman tables are decoded through an 8-bit prefix table with sequential extension beyond 8 bits; byte stuffing (FF 00) and restart markers are handled in the bit reader. The difference category SSSS yields the prediction error
added modulo dcmCmp confirms that all 9,000,000 pixels of every pair decode identically.
Tools/DictGen parses the DocBook XML source part06.xml, which NEMA publishes directly [2][5] — there is no official GitHub repository and no official reference implementation — and emits LUX.DICOM.Tags.pas (≈5,300 entries from Tables 6-1, 7-1, 8-1, 9-1) and LUX.DICOM.UIDs.pas (≈470 entries from Table A-1) as pure const arrays. Wildcard tags such as (60xx,3000) are normalized to a key/mask pair matched by
The standard is revised about five times a year; following a revision is: download the current part06.xml, run DictGen part06.xml ..\..\Dictio, rebuild, and re-run the regression tools. Should a new VR appear in the tables, the generator lists it and exits with a warning — adding one enum value and one _VRInfo_ row is the only manual step.
uses LUX.DICOM; // 文字集合・辞書・JPEG ロスレスもこの uses だけで有効になる
procedure Demo( const FileName_:String );
var
F :TdcmFile;
E :TdcmElement;
WC, WW :Double;
begin
F := TdcmFile.Create( FileName_ );
try
Writeln( F.Syntax.Name ); // e.g. 'JPEG Lossless (Process 14, SV1)'
Writeln( F.Body.GetText( $0010, $0010 ) ); // PatientName — ISO 2022 IR 87 decoded
for E in F.Body do Writeln( E.Tag.ToString, ' ', E.VRText, ' ', E.Text( F.Body.Charse ) );
if F.HasPixels then
begin
F.Pixels.DefaultWindow( 0, WC, WW ); // tag-supplied window, else measured
{ F.Pixels.FrameRaw(0) + F.Pixels.BuildLUT8(WC,WW), or
LUX.DICOM.Stream.FMX.DcmFrameToBitmap( F.Pixels, 0, WC, WW, Bitmap ) }
end;
finally
F.Free;
end;
end;| Tool | Checks |
|---|---|
dcmTest |
88 unit tests: tag order, VR table round trip, syntax resolution, dictionary and wildcard lookup, ISO 2022 decoding, synthetic implicit / nested-SQ / truncated streams, bit extraction (3) |
dcmDump |
tag-tree dump of the 8 JIRA samples — all parse with zero issues |
dcmPix |
windowed BMP rendering of uncompressed samples |
dcmCmp |
JPEG ↔ uncompressed pixel equality, 4 pairs × 9 M pixels, all identical |
dcmRT |
read → write → read: every element of all 8 samples, including fragments, byte-identical |
- NEMA, DICOM PS3.5 — Data Structures and Encoding, §6.2 Value Representation, §7.1 Data Elements, §7.5 Nesting of Data Sets, §A.4 Encapsulation.
- NEMA, DICOM PS3.6 — Data Dictionary, and its DocBook source.
- NEMA, DICOM PS3.3 — Information Object Definitions, §C.7.6.3 Image Pixel Module, §C.11.2 VOI LUT Module.
- ITU-T, Recommendation T.81 — Digital compression and coding of continuous-tone still images, Annex H, Lossless mode of operation.
- NEMA, DICOM Standard — Current Edition.
Integrated Development Environment (IDE) for Creating Native Cross-Platform Apps.