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 ClassUIDMIME TypeUse Case
Encapsulated PDF1.2.840.10008.5.1.4.1.1.104.1application/pdfReports, forms
Encapsulated CDA1.2.840.10008.5.1.4.1.1.104.2text/xmlHL7 Clinical Documents
Encapsulated STL1.2.840.10008.5.1.4.1.1.104.3model/stl3D printing models
Encapsulated OBJ1.2.840.10008.5.1.4.1.1.104.4model/obj3D surface models
Encapsulated MTL1.2.840.10008.5.1.4.1.1.104.5model/mtl3D 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.