DICOMHERO can be used in Swift apps running on iOS or macOS.

The source code can be compiled into a framework, or you can use the pre-compiled XCFramework containing the binaries for several platforms and architectures (macOS arm64 & x64, iOS arm64, iOS simulator arm64 & x64).


Download the XCFramework

First download the pre-compiled XCFramework from the downloads page and copy the dicomhero6.xcframework folder from the downloaded DMG into your local hard drive.

macOS applies the quarantine flag to unsigned files downloaded from the internet, preventing them from working. Remove the quarantine flag with the following command:

xattr -r -d com.apple.quarantine path/to/dicomhero6.xcframework

where path/to/dicomhero6.xcframework is the path to the dicomhero6.xcframework folder copied from the downloaded DMG file.


Create the iOS app

Open Xcode, click on File->New->Project, then create a new iOS app:
XCode create sample ios app

Select the name DicomHeroIOSSample:
Xcode name sample iOS app.

Then let Xcode create the App.


Add the DICOMHERO XCFramework to the app

Once the app code appears in Xcode, add the DICOMHERO framework to the app.
Click the + symbol in “Add Frameworks, libraries and embedded content here” in the General app properties:
Xcode add framework
then select “Add Other”, select “Files” and select the dicomhero6.xcframework folder selected earlier:
Xcode select framework type
The DICOMHERO framework will appear in the frameworks list:
Xcode with DICOMHERO framework.


Create the bridging header

The DICOMHERO framework contains ObjectiveC headers, so we have to create a Swift bridging header in order to use the DICOMHERO headers in our project.

First add a new header file to your app named DicomHeroIOSSample-bridging-header.h to your project. The file will be used to import the ObjectiveC headers and make them available to our Swift app.
Click File->New->File on the Xcode menu, then add a new Header file:
Xcode create new bridging header
then name the file DicomHeroIOSSample-bridging-header.h and click on Create:
Xcode name bridging header.

In the newly created bridging header file, add the line #import <dicomhero6/dicomhero6> to import the DICOMHERO ObjectiveC header and make it available to the Swift app.
Xcode include ObjectiveC header

Finally, specify in the Build Settings that the new header file must be used as bridging header for Swift:
Xcode specify bridging header in build settings


Modify the ContentView file to display a DICOM image

We are going to modify the ContentView.swift file so it displays a button to load a DICOM file and a placeholder for the image embedded in the DICOM file as well as a placeholder for the patient name.

//
//  ContentView.swift
//  DicomHeroIOSSample
//
//  Created by Paolo Brandoli on 25/08/2023.
//

import SwiftUI
import UniformTypeIdentifiers

/*
 Stores the data loaded from a dicom file
 */
struct DicomData
{
    // Patient name
    var patientName = ""
    
    // Image
    var image: UIImage?
}


/*
 Document Picker View
 */
struct DocumentPickerImportView: UIViewControllerRepresentable {
    
    @Binding var path: URL       ///< Path of the selected file
    @Binding var loading: Bool   ///< Set to true while the file is being loaded
    @Binding var data: DicomData ///< Loaded data
    
    func makeCoordinator() -> DocumentPickerCoordinator {
        return DocumentPickerCoordinator(path: $path, loading: $loading, data: $data)
    }
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<DocumentPickerImportView>) -> UIDocumentPickerViewController
    {
        let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [UTType.item], asCopy: false)
        documentPicker.delegate = context.coordinator
        return documentPicker
    }
    
    func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context)
    {
    }
}


class DocumentPickerCoordinator: NSObject, UIDocumentPickerDelegate, UINavigationControllerDelegate
{
    @Binding var path: URL       ///< Path of the selected file
    @Binding var loading: Bool   ///< Set to true while the file is being loaded
    @Binding var data: DicomData ///< Loaded data
    
    init(path: Binding<URL>, loading: Binding<Bool>, data: Binding<DicomData>)
    {
        self._path = path
        self._loading = loading
        self._data = data
    }
    
    
    /**
     The user has picked the file. Launch a secondary thread that loads it
     */
    func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) {
        path = url
        
        // Load the document from a secondary thread
        DispatchQueue.global(qos: .background).async
        {
            self.loadImage(url: url)
        }
    }
    
    
    /*
     Load the dataset. This is launched in a secondary thread
     */
    private func loadImage(url:URL)
    {
        // Notify that the loading operation is in progress
        DispatchQueue.main.async {
            self.loading = true
        }
        
        // Access the url
        guard url.startAccessingSecurityScopedResource() else {
            return
        }
        
        // Remember to reset the loading flag and the access to the url
        defer
        {
            DispatchQueue.main.async {
                self.loading = false
            }
            url.stopAccessingSecurityScopedResource()
        }
        
        do
        {
            // Set the maximum image size. DICOMHERO has a default limit which prevent corrupted files
            // from forcing DICOMHERO to allocate a huge quantity of memory
            DicomheroCodecFactory.setMaximumImageSize(8000, maxHeight: 8000)
            
            // Load the dataset from the picked file
            let dataset: DicomheroDataSet = try DicomheroCodecFactory.load(fromFileMaxSize: url.path, maxBufferSize: 2048)
            
            // Get the image with the modality transform applied
            let heroImage: DicomheroImage = try dataset.getImageApplyModalityTransform(0)
            
            // Get the image size
            let width = heroImage.width
            let height = heroImage.height
            
            // Build the transforms chain.
            // The chain will list all the transforms needed to arrive from the original image to a visible one
            // (color transform, VOI/LUT, etc)
            let chain = DicomheroTransformsChain()
            
            if DicomheroColorTransformsFactory.isMonochrome(heroImage.colorSpace)
            {
                // Get the VOI listed in the dataset. If VOIs are present then use the first one
                let vois = try dataset.getVOIs() as! Array<DicomheroVOIDescription>;
                if !vois.isEmpty
                {
                    chain!.add(DicomheroVOILUT(voiDescription: vois.first))
                }
                else
                {
                    // VOIs not present, calculate the optimal one
                    let voiDescription = try DicomheroVOILUT.getOptimalVOI(heroImage, inputTopLeftX: 0, inputTopLeftY: 0, inputWidth: width, inputHeight: height)
                    chain!.add(DicomheroVOILUT(voiDescription: voiDescription));
                }
            }
            
            // Build the draw object with the proper transforms chain
            let draw = DicomheroDrawBitmap(transform: chain)
            
            // Draw the image into an UIImage or NSImage
            let image = try draw?.getDicomheroImage(heroImage)
            
            // Get the patient name
            var patientName = ""
            do
            {
                let patientNameStructure = try dataset.getPatientName(DicomheroTagId(id: DicomheroTagEnum.enumPatientName_0010_0010), elementNumber: 0)
                patientName = patientNameStructure.alphabeticRepresentation
            }
            catch
            {
                print("caught: \(error)")
            }
            
            // Pass the data back (from the main thread)
            DispatchQueue.main.async {
                self.data.image = image
                self.data.patientName = patientName
            }
        }
        catch
        {
            print("caught: \(error)")
        }
    }
}


/*
 Main View
 */
struct ContentView: View {
    
    // true when the file picker must appear
    @State var showFilePicker: Bool = false
    
    // selected path
    @State var path: URL = URL(fileURLWithPath: "")
    
    // true if a loading is in progress
    @State var loading = false
    
    // loaded data
    @State var data = DicomData(image: nil)
    
    
    var body: some View {
        VStack {
            
            // Load button
            Button(action:
                    {
                self.showFilePicker.toggle()
            })
            {
                Label("Load dataset", systemImage: "doc.text.image").frame(maxWidth: .infinity)
            }
            .buttonStyle(.bordered).padding([.leading, .trailing])
            .disabled(loading)
            
            // Patient name
            Text(data.patientName)
            
            // Image
            if data.image != nil
            {
                Image(uiImage: data.image!).resizable()
                    .scaledToFit().imageScale(.medium)
            }
        }
        .padding()
        .sheet(isPresented: self.$showFilePicker)
        {
            DocumentPickerImportView(path: $path, loading: $loading, data: $data)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}


Run the app!

Comments are closed