Understanding DICOM binary encoding is essential for low-level file manipulation and debugging.
Binary File Structure
DICOM File Binary Layout
========================
Offset 0-127: File Preamble (128 bytes)
Usually zeros, can contain application data
Offset 128-131: DICOM Prefix "DICM"
ASCII bytes: 0x44, 0x49, 0x43, 0x4D
Offset 132+: File Meta Information (Group 0002)
Always Explicit VR Little Endian
Offset N+: Dataset
Encoded per Transfer Syntax in (0002,0010)
Explicit VR vs Implicit VR Encoding
Explicit VR Little Endian
Explicit VR Data Element (Short Form)
=====================================
For VRs: AE, AS, AT, CS, DA, DS, DT, FL, FD, IS, LO, LT, PN, SH, SL, SS, ST, TM, UI, UL, US
Bytes 0-1: Group Number (little endian)
Bytes 2-3: Element Number (little endian)
Bytes 4-5: Value Representation (2 ASCII chars)
Bytes 6-7: Value Length (2 bytes, little endian)
Bytes 8+: Value
Example: Patient Name "Doe^John"
Explicit VR Data Element (Long Form)
For VRs: OB, OD, OF, OL, OW, SQ, UC, UN, UR, UT
- Bytes 0-1: Group Number
- Bytes 2-3: Element Number
- Bytes 4-5: Value Representation
- Bytes 6-7: Reserved (0x0000)
- Bytes 8-11: Value Length (4 bytes)
- Bytes 12+: Value
Example: Pixel Data
Implicit VR Little Endian
Implicit VR Data Element
========================
Bytes 0-1: Group Number (little endian)
Bytes 2-3: Element Number (little endian)
Bytes 4-7: Value Length (4 bytes)
Bytes 8+: Value
Example: Patient Name "Doe^John"
Note: VR is NOT included — must be looked up in dictionary.
Encoding Comparison
public class EncodingExample
{
public void DemonstrateEncoding()
{
// Data to encode: Patient Name = "Doe^John"
string tagGroup = "0010";
string tagElement = "0010";
string vr = "PN";
string value = "Doe^John";
Console.WriteLine("Explicit VR Little Endian:");
byte[] explicitVR = EncodeExplicitVR(tagGroup, tagElement, vr, value);
Console.WriteLine(BitConverter.ToString(explicitVR));
// Output: 10-00-10-00-50-4E-08-00-44-6F-65-5E-4A-6F-68-6E
Console.WriteLine("\nImplicit VR Little Endian:");
byte[] implicitVR = EncodeImplicitVR(tagGroup, tagElement, value);
Console.WriteLine(BitConverter.ToString(implicitVR));
// Output: 10-00-10-00-08-00-00-00-44-6F-65-5E-4A-6F-68-6E
}
private byte[] EncodeExplicitVR(string group, string element, string vr, string value)
{
using (var ms = new MemoryStream())
using (var writer = new BinaryWriter(ms))
{
// Group (Little Endian)
writer.Write(Convert.ToUInt16(group, 16));
// Element (Little Endian)
writer.Write(Convert.ToUInt16(element, 16));
// VR (2 ASCII chars)
writer.Write(Encoding.ASCII.GetBytes(vr));
// Length (2 bytes for short-form VRs)
writer.Write((ushort)value.Length);
// Value
writer.Write(Encoding.ASCII.GetBytes(value));
return ms.ToArray();
}
}
private byte[] EncodeImplicitVR(string group, string element, string value)
{
using (var ms = new MemoryStream())
using (var writer = new BinaryWriter(ms))
{
// Group
writer.Write(Convert.ToUInt16(group, 16));
// Element
writer.Write(Convert.ToUInt16(element, 16));
// Length (4 bytes in Implicit VR)
writer.Write((uint)value.Length);
// Value
writer.Write(Encoding.ASCII.GetBytes(value));
return ms.ToArray();
}
}
}
Sequence Encoding
Sequences contain nested items, each with their own data elements.
Sequence Delimiter Tags
| Tag | Name | Purpose |
|---|---|---|
| (FFFE,E000) | Item | Start of sequence item |
| (FFFE,E00D) | Item Delimitation Item | End of item (undefined length) |
| (FFFE,E0DD) | Sequence Delimitation Item | End of sequence (undefined length) |
Pixel Data Encoding
Uncompressed (Native) Pixel Data
Native Pixel Data
=================
Tag: (7FE0,0010)
VR: OW or OB
Length: rows x columns x bytesPerPixel x samplesPerPixel
Example: 512x512, 16-bit, Monochrome
Length = 512 * 512 * 2 * 1 = 524,288 bytes
Binary layout:
Encapsulated (Compressed) Pixel Data
Encapsulated Pixel Data
=======================
Used for: JPEG, JPEG 2000, JPEG-LS, RLE compression
Tag: (7FE0,0010)
VR: OB
Length: FFFFFFFF (undefined)
Structure:
Low-Level Parser Example
public class LowLevelDicomParser
{
public void ParseDicomFile(string filePath)
{
using (FileStream fs = new FileStream(filePath, FileMode.Open))
using (BinaryReader reader = new BinaryReader(fs))
{
// Skip preamble (128 bytes)
reader.ReadBytes(128);
// Read DICM prefix
byte[] prefix = reader.ReadBytes(4);
if (Encoding.ASCII.GetString(prefix) != "DICM")
{
Console.WriteLine("Not a valid DICOM file");
return;
}
// Read File Meta Information (always Explicit VR LE)
Console.WriteLine("=== File Meta Information ===");
ParseElements(reader, explicitVR: true, stopGroup: 0x0008);
// Determine transfer syntax
string transferSyntax = GetTransferSyntax();
bool isExplicitVR = transferSyntax != "1.2.840.10008.1.2";
Console.WriteLine("\n=== Dataset ===");
ParseElements(reader, isExplicitVR);
}
}
private void ParseElements(BinaryReader reader, bool explicitVR, ushort? stopGroup = null)
{
while (reader.BaseStream.Position < reader.BaseStream.Length)
{
ushort group = reader.ReadUInt16();
ushort element = reader.ReadUInt16();
if (stopGroup.HasValue && group >= stopGroup.Value)
{
reader.BaseStream.Position -= 4;
break;
}
string vr = "";
uint length;
if (explicitVR)
{
vr = Encoding.ASCII.GetString(reader.ReadBytes(2));
// Check for long-form VRs
bool isLongForm = vr == "OB" || vr == "OD" || vr == "OF" ||
vr == "OL" || vr == "OW" || vr == "SQ" ||
vr == "UC" || vr == "UN" || vr == "UR" || vr == "UT";
if (isLongForm)
{
reader.ReadUInt16(); // Reserved
length = reader.ReadUInt32();
}
else
{
length = reader.ReadUInt16();
}
}
else
{
length = reader.ReadUInt32();
vr = LookupVR(group, element);
}
// Handle undefined length (sequences, encapsulated data)
if (length == 0xFFFFFFFF)
{
Console.WriteLine($"({group:X4},{element:X4}) {vr} = (undefined length)");
// Would need to parse until delimiter
break;
}
// Read value
byte[] value = reader.ReadBytes((int)length);
// Display
string valueStr = FormatValue(vr, value);
Console.WriteLine($"({group:X4},{element:X4}) {vr} [{length}] = {valueStr}");
// Stop at pixel data
if (group == 0x7FE0 && element == 0x0010)
{
Console.WriteLine("... (pixel data)");
break;
}
}
}
private string LookupVR(ushort group, ushort element)
{
// Simplified - would use full DICOM dictionary
string tag = $"{group:X4}{element:X4}";
return tag switch
{
"00100010" => "PN",
"00100020" => "LO",
"00080020" => "DA",
"00080060" => "CS",
_ => "UN"
};
}
private string FormatValue(string vr, byte[] value)
{
if (value.Length == 0) return "(empty)";
return vr switch
{
"UI" or "LO" or "SH" or "PN" or "CS" or "DA" or "TM" or "AE" =>
Encoding.ASCII.GetString(value).Trim('\0', ' '),
"US" => value.Length >= 2 ? BitConverter.ToUInt16(value, 0).ToString() : "(invalid)",
"UL" => value.Length >= 4 ? BitConverter.ToUInt32(value, 0).ToString() : "(invalid)",
"SQ" => "(sequence)",
"OB" or "OW" => $"(binary, {value.Length} bytes)",
_ => $"(binary, {value.Length} bytes)"
};
}
private string GetTransferSyntax()
{
// Would be stored from parsing (0002,0010)
return "1.2.840.10008.1.2.1"; // Explicit VR LE default
}
}
Understanding DICOM binary encoding enables debugging, custom parsing, and low-level file manipulation.