DICOM Basics using .NET and C# - Encapsulated Documents (PDF, CDA, STL)
Introduction
This is part of my series of articles on the DICOM standard. In this tutorial, we'll explore DICOM encapsulated documents, which allow you to store non-DICOM documents like PDFs, clinical documents (CDA), and 3D printing models alongside images in your PACS.
Encapsulated documents are essential for comprehensive patient records, enabling radiology reports, consent forms, and even 3D anatomical models to be stored and retrieved through standard DICOM interfaces.
Prerequisites
Before you begin, ensure you have the following:
- A .NET development environment (Visual Studio or Visual Studio Code)
- The Fellow Oak DICOM library (fo-dicom) installed via NuGet
- Basic understanding of DICOM concepts from previous tutorials
- You can find all the code demonstrated in this tutorial on GitHub here
“The meaning of life is to find your gift. The purpose of life is to give it away.” ~ Pablo Picasso
The Theory Behind Encapsulated Documents
Medical imaging doesn't exist in isolation. A CT study includes the images, but also the radiologist's report, prior authorization, consent forms, and perhaps 3D reconstructions for surgical planning. Historically, these lived in separate systems: images in PACS, reports in document management, 3D models in specialized software. Encapsulated documents solve this document unification problem by wrapping non-DICOM content in DICOM packaging.
The wrapper concept is elegantly simple: take any document (PDF, XML, STL), add DICOM metadata that provides context (patient, study, what the document is), and store the original bytes unchanged. The document remains fully extractable in its original format, but gains all the benefits of DICOM: it can be queried by patient/study, retrieved through standard DICOM operations, and stored in the PACS alongside related images.
For XDS-I (Cross-Enterprise Document Sharing for Imaging), encapsulated documents are essential. When sharing imaging studies across institutions, the report must travel with the images. By encapsulating the PDF report as a DICOM object linked to the study, both can be transmitted and accessed through the same IHE infrastructure.
The 3D printing SOP classes (STL, OBJ, MTL) reflect healthcare's evolving use of additive manufacturing. From anatomical models for surgical planning to patient-specific implants, 3D printing requires precise geometric data. By encapsulating these models in DICOM, they become part of the patient record with full traceability to the imaging data they were derived from.
The Burned In Annotation attribute addresses the de-identification challenge. Unlike pixel data where anonymization tools can modify DICOM attributes, the encapsulated content is opaque bytes. Setting Burned In Annotation = YES signals that the document contains information that cannot be removed through standard DICOM anonymization, alerting downstream systems to handle it appropriately.
Encapsulated Document SOP Classes
DICOM defines several SOP Classes for encapsulating different document types:
| SOP Class | UID | MIME Type | Use Case |
|---|---|---|---|
| Encapsulated PDF | 1.2.840.10008.5.1.4.1.1.104.1 | application/pdf | Reports, forms |
| Encapsulated CDA | 1.2.840.10008.5.1.4.1.1.104.2 | text/xml | HL7 Clinical Documents |
| Encapsulated STL | 1.2.840.10008.5.1.4.1.1.104.3 | model/stl | 3D printing models |
| Encapsulated OBJ | 1.2.840.10008.5.1.4.1.1.104.4 | model/obj | 3D surface models |
| Encapsulated MTL | 1.2.840.10008.5.1.4.1.1.104.5 | model/mtl | 3D model materials |
Step 1 of 4: Creating an Encapsulated PDF
Here's how to create a DICOM object containing a PDF document:
using System;
using System.Diagnostics;
using System.IO;
using FellowOakDicom;
namespace DicomEncapsulatedDocuments
{
public class Program
{
private static readonly string OutputPath =
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Output");
public static void Main(string[] args)
{
try
{
LogToDebugConsole("=== DICOM Encapsulated Documents Demo ===");
// Ensure output directory exists
if (!Directory.Exists(OutputPath))
{
Directory.CreateDirectory(OutputPath);
}
// Create an encapsulated PDF
CreateEncapsulatedPdf();
}
catch (Exception e)
{
LogToDebugConsole($"Error: {e.Message}");
}
}
private static void CreateEncapsulatedPdf()
{
// Read actual PDF file (or use sample data for demo)
// byte[] pdfData = File.ReadAllBytes("report.pdf");
byte[] pdfData = CreateSamplePdfPlaceholder();
var dataset = new DicomDataset();
string currentDate = DateTime.Now.ToString("yyyyMMdd");
string currentTime = DateTime.Now.ToString("HHmmss");
//---------------------------------------------------------------
// SOP Common Module
//---------------------------------------------------------------
dataset.Add(DicomTag.SOPClassUID, DicomUID.EncapsulatedPDFStorage);
dataset.Add(DicomTag.SOPInstanceUID, DicomUID.Generate());
//---------------------------------------------------------------
// Patient Module
//---------------------------------------------------------------
dataset.Add(DicomTag.PatientName, "Doe^John");
dataset.Add(DicomTag.PatientID, "PAT123");
dataset.Add(DicomTag.PatientBirthDate, "19700101");
dataset.Add(DicomTag.PatientSex, "M");
//---------------------------------------------------------------
// General Study Module
//---------------------------------------------------------------
dataset.Add(DicomTag.StudyInstanceUID, DicomUID.Generate());
dataset.Add(DicomTag.StudyDate, currentDate);
dataset.Add(DicomTag.StudyTime, currentTime);
dataset.Add(DicomTag.AccessionNumber, "ACC456");
dataset.Add(DicomTag.ReferringPhysicianName, "Smith^Jane^Dr");
dataset.Add(DicomTag.StudyID, "STUDY001");
//---------------------------------------------------------------
// Encapsulated Document Series Module
//---------------------------------------------------------------
dataset.Add(DicomTag.Modality, "DOC"); // Document modality
dataset.Add(DicomTag.SeriesInstanceUID, DicomUID.Generate());
dataset.Add(DicomTag.SeriesNumber, "1");
//---------------------------------------------------------------
// SC Equipment Module
//---------------------------------------------------------------
dataset.Add(DicomTag.ConversionType, "WSD"); // Workstation
//---------------------------------------------------------------
// Encapsulated Document Module
//---------------------------------------------------------------
dataset.Add(DicomTag.InstanceNumber, "1");
dataset.Add(DicomTag.ContentDate, currentDate);
dataset.Add(DicomTag.ContentTime, currentTime);
dataset.Add(DicomTag.AcquisitionDateTime, currentDate + currentTime);
// Burned In Annotation - important for privacy
dataset.Add(DicomTag.BurnedInAnnotation, "YES");
// Document Title
dataset.Add(DicomTag.DocumentTitle, "Chest CT Report");
// MIME Type - critical for proper handling
dataset.Add(DicomTag.MIMETypeOfEncapsulatedDocument, "application/pdf");
// The actual PDF bytes
dataset.Add(new DicomOtherByte(DicomTag.EncapsulatedDocument, pdfData));
//---------------------------------------------------------------
// Save the DICOM file
//---------------------------------------------------------------
string outputFile = Path.Combine(OutputPath, "encapsulated_pdf.dcm");
var dicomFile = new DicomFile(dataset);
dicomFile.Save(outputFile);
LogToDebugConsole($"Encapsulated PDF created: {outputFile}");
LogToDebugConsole($"Document size: {pdfData.Length} bytes");
}
private static byte[] CreateSamplePdfPlaceholder()
{
// In real use: return File.ReadAllBytes(pdfFilePath);
return new byte[] { 0x25, 0x50, 0x44, 0x46 }; // %PDF header
}
private static void LogToDebugConsole(string message)
{
Debug.WriteLine(message);
}
}
}
Step 2 of 4: Key Attributes for Encapsulated Documents
The essential attributes for encapsulated documents:
private static void DemonstrateEncapsulatedPdfStructure()
{
LogToDebugConsole("Key Attributes for Encapsulated PDF:");
LogToDebugConsole("");
LogToDebugConsole(" (0008,0016) SOP Class UID = 1.2.840.10008.5.1.4.1.1.104.1");
LogToDebugConsole(" (0008,0060) Modality = DOC");
LogToDebugConsole(" (0008,0064) Conversion Type = WSD");
LogToDebugConsole(" (0028,0301) Burned In Annotation = YES");
LogToDebugConsole(" (0042,0010) Document Title = (your title)");
LogToDebugConsole(" (0042,0012) MIME Type = application/pdf");
LogToDebugConsole(" (0042,0011) Encapsulated Document = (PDF bytes)");
LogToDebugConsole("");
LogToDebugConsole("Required Modules:");
LogToDebugConsole(" - Patient Module");
LogToDebugConsole(" - General Study Module");
LogToDebugConsole(" - Encapsulated Document Series Module");
LogToDebugConsole(" - SC Equipment Module");
LogToDebugConsole(" - Encapsulated Document Module");
LogToDebugConsole(" - SOP Common Module");
}
Step 3 of 4: Encapsulating Other Document Types
For CDA documents and 3D models, adjust the SOP Class and MIME type:
/// <summary>
/// Create Encapsulated CDA (Clinical Document Architecture)
/// </summary>
private static void CreateEncapsulatedCda()
{
var dataset = new DicomDataset();
// Use CDA SOP Class
dataset.Add(DicomTag.SOPClassUID, DicomUID.EncapsulatedCDAStorage);
dataset.Add(DicomTag.SOPInstanceUID, DicomUID.Generate());
// ... patient, study, series modules ...
dataset.Add(DicomTag.Modality, "DOC");
dataset.Add(DicomTag.MIMETypeOfEncapsulatedDocument, "text/xml");
// CDA content (XML)
byte[] cdaData = File.ReadAllBytes("clinicalDocument.xml");
dataset.Add(new DicomOtherByte(DicomTag.EncapsulatedDocument, cdaData));
}
/// <summary>
/// Create Encapsulated STL for 3D printing
/// </summary>
private static void CreateEncapsulatedStl()
{
var dataset = new DicomDataset();
// Use STL SOP Class
dataset.Add(DicomTag.SOPClassUID, DicomUID.EncapsulatedSTLStorage);
dataset.Add(DicomTag.SOPInstanceUID, DicomUID.Generate());
// ... patient, study, series modules ...
dataset.Add(DicomTag.Modality, "M3D"); // 3D Printing modality
dataset.Add(DicomTag.MIMETypeOfEncapsulatedDocument, "model/stl");
// STL 3D model data
byte[] stlData = File.ReadAllBytes("anatomy.stl");
dataset.Add(new DicomOtherByte(DicomTag.EncapsulatedDocument, stlData));
}
Step 4 of 4: Privacy Considerations
Encapsulated documents require special attention for privacy:
private static void DemonstratePrivacyConsiderations()
{
LogToDebugConsole("Privacy Considerations for Encapsulated Documents:");
LogToDebugConsole("");
LogToDebugConsole("1. Burned In Annotation");
LogToDebugConsole(" - Set to YES if document contains PHI");
LogToDebugConsole(" - Alerts downstream systems to sensitive content");
LogToDebugConsole("");
LogToDebugConsole("2. Document Content");
LogToDebugConsole(" - PDF text cannot be anonymized by DICOM tools");
LogToDebugConsole(" - Consider using redacted PDFs for teaching/research");
LogToDebugConsole(" - May need manual review before sharing");
LogToDebugConsole("");
LogToDebugConsole("3. Document Title");
LogToDebugConsole(" - Should not contain PHI in the title attribute");
LogToDebugConsole(" - Use generic descriptions");
LogToDebugConsole("");
LogToDebugConsole("4. De-identification");
LogToDebugConsole(" - Standard DICOM de-identification won't remove PDF content");
LogToDebugConsole(" - Must process document separately if needed");
LogToDebugConsole(" - Consider PDF redaction tools for sensitive content");
}
Reading Encapsulated Documents
To extract the encapsulated document from a DICOM file:
public static void ExtractEncapsulatedDocument(string dicomPath, string outputPath)
{
var file = DicomFile.Open(dicomPath);
var dataset = file.Dataset;
// Check SOP Class
var sopClass = dataset.GetSingleValueOrDefault(DicomTag.SOPClassUID, "");
LogToDebugConsole($"SOP Class: {sopClass}");
// Get MIME type to determine file extension
var mimeType = dataset.GetSingleValueOrDefault(
DicomTag.MIMETypeOfEncapsulatedDocument, "");
LogToDebugConsole($"MIME Type: {mimeType}");
// Get document title
var title = dataset.GetSingleValueOrDefault(DicomTag.DocumentTitle, "");
LogToDebugConsole($"Document Title: {title}");
// Extract encapsulated document
var docElement = dataset.GetDicomItem<DicomOtherByte>(DicomTag.EncapsulatedDocument);
if (docElement != null)
{
byte[] documentData = docElement.Get<byte[]>();
// Determine extension from MIME type
string extension = mimeType switch
{
"application/pdf" => ".pdf",
"text/xml" => ".xml",
"model/stl" => ".stl",
"model/obj" => ".obj",
_ => ".bin"
};
File.WriteAllBytes(outputPath + extension, documentData);
LogToDebugConsole($"Extracted document: {outputPath}{extension}");
}
}
Common Use Cases
- Radiology Reports: PDF reports stored with corresponding images
- Consent Forms: Signed consent documents linked to studies
- Lab Results: CDA documents from laboratory systems
- 3D Printing: STL/OBJ models for surgical planning or prosthetics
- External Reports: Scanned documents from referring physicians
Best Practices
- Always set Burned In Annotation: Indicate whether PHI is in the document
- Use meaningful Document Titles: But avoid PHI in the title
- Link to related images: Use Referenced Series Sequence when applicable
- Consider file size: Large documents may impact PACS performance
- Verify MIME types: Ensure correct MIME type for proper viewer handling
Conclusion
Encapsulated documents extend DICOM beyond imaging to support complete patient records. By wrapping PDFs, clinical documents, and even 3D models in DICOM format, healthcare organizations can manage all patient-related content through their existing PACS infrastructure.
The key to successful implementation is understanding the privacy implications - unlike pixel data that can be anonymized through DICOM tools, encapsulated document content requires separate handling for de-identification purposes.
Please check out the next tutorial in this series where we cover DICOM Presentation States.