diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a54caf1..f96700c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,9 +10,8 @@ jobs: fail-fast: false matrix: version: - - '1.0' - - '1' - - 'nightly' + - "1" + - "nightly" os: - ubuntu-latest - macOS-latest diff --git a/Project.toml b/Project.toml index 2f636b8..bdd5ed5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,12 +1,13 @@ name = "DICOM" uuid = "a26e6606-dd52-5f6a-a97f-4f611373d757" -version = "0.10.1" +version = "0.11.0" [compat] -julia = "0.7, 1" +julia = "1.6" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" [targets] -test = ["Test"] +test = ["Test", "Downloads"] diff --git a/src/DICOM.jl b/src/DICOM.jl index 64bf64a..5e73121 100644 --- a/src/DICOM.jl +++ b/src/DICOM.jl @@ -592,7 +592,7 @@ function write_element(st::IO, gelt::Tuple{UInt16,UInt16}, data, is_explicit, au if vr == "SQ" vr = is_explicit ? vr : empty_vr - return dcm_store(st, gelt, s -> sequence_write(s, map(d -> d.meta, data), is_explicit), vr) + return dcm_store(st, gelt, s -> sequence_write(s, map(d -> d.meta, data), is_explicit, aux_vr), vr) end # Pack data into array container. This is to undo "data = data[1]" from read_element(). @@ -658,27 +658,27 @@ function dcm_store(st::IO, gelt::Tuple{UInt16,UInt16}, writef::Function, vr::Str sz += 1 end seek(st, p) - write(st, convert(lentype, max(0, sz))) + vr == "SQ" || gelt == (0xFFFE, 0xE000) ? write(st, convert(lentype, 0xffffffff)) : write(st, convert(lentype, max(0, sz))) seek(st, endp) if szWasOdd - write(st, UInt8(0)) + vr in ("AE", "CS", "SH", "LO", "PN", "DA", "DT", "TM") ? write(st, UInt8(0x20)) : write(st, UInt8(0)) end end -sequence_write(st::IO, items::Array{Any,1}, evr::Bool) = - sequence_write(st, convert(Array{Dict{Tuple{UInt16,UInt16},Any},1}, items), evr) -function sequence_write(st::IO, items::Array{Dict{Tuple{UInt16,UInt16},Any},1}, evr) +sequence_write(st::IO, items::Array{Any,1}, evr::Bool, aux_vr::Dict{Tuple{UInt16, UInt16}}) = + sequence_write(st, convert(Array{Dict{Tuple{UInt16,UInt16},Any},1}, items), evr, aux_vr) +function sequence_write(st::IO, items::Array{Dict{Tuple{UInt16,UInt16},Any},1}, evr, aux_vr::Dict{Tuple{UInt16, UInt16}}) for subitem in items if length(subitem) > 0 - dcm_store(st, (0xFFFE, 0xE000), s -> sequence_item_write(s, subitem, evr)) + dcm_store(st, (0xFFFE, 0xE000), s -> sequence_item_write(s, subitem, evr, aux_vr)) end end write(st, UInt16[0xFFFE, 0xE0DD, 0x0000, 0x0000]) end -function sequence_item_write(st::IO, items::Dict{Tuple{UInt16,UInt16},Any}, evr) +function sequence_item_write(st::IO, items::Dict{Tuple{UInt16,UInt16},Any}, evr, aux_vr::Dict{Tuple{UInt16, UInt16}}) for gelt in sort(collect(keys(items))) - write_element(st, gelt, items[gelt], evr, empty_dcm_dict) + write_element(st, gelt, items[gelt], evr, aux_vr) end write(st, UInt16[0xFFFE, 0xE00D, 0x0000, 0x0000]) end diff --git a/test/runtests.jl b/test/runtests.jl index 42fd148..a88b64c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,6 @@ using Test using DICOM +using Downloads const data_folder = joinpath(@__DIR__, "testdata") if !isdir(data_folder) @@ -26,7 +27,7 @@ const dicom_samples = Dict( "US_Explicit_Big_RGB.dcm" => "https://github.com/notZaki/DICOMSamples/raw/master/DICOMSamples/US_Explicit_Big_RGB.dcm", "DX_Implicit_Little_Interleaved.dcm" => - "https://github.com/OHIF/viewer-testdata/raw/master/dcm/zoo-exotic/5.dcm", + "https://github.com/notZaki/DICOMSamples/raw/master/DICOMSamples/DX_Implicit_Little_Interleaved.dcm", ) function download_dicom(filename; folder = data_folder) @@ -34,7 +35,7 @@ function download_dicom(filename; folder = data_folder) url = dicom_samples[filename] filepath = joinpath(folder, filename) if !isfile(filepath) - download(url, filepath) + Downloads.download(url, filepath) end return filepath end @@ -233,8 +234,8 @@ end end # First, test the isdicom() function - fileDX = download_dicom("DX_Implicit_Little_Interleaved.dcm") - @test DICOM.isdicom(fileDX) == true + fileCT = download_dicom("CT_JPEG70.dcm") + @test DICOM.isdicom(fileCT) == true @test DICOM.isdicom(notdicomfile) == false # Second, test if all valid dicom file can be parsed @@ -255,3 +256,53 @@ end @test macroexpand(Main, :(tag"Modality")) === (0x0008, 0x0060) @test_throws LoadError macroexpand(Main, :(tag"nonsense")) end + +@testset "Test VR in Sequence" begin + # Test auxillary VR passed to nested tags + fileMG = download_dicom("MG_Explicit_Little.dcm") + dcmMG = dcm_parse(fileMG) + # Set value of CodeMeaning for both tags to something arbitrary + codemeaning_vr = Dict{Tuple{UInt16, UInt16}, String}((0x008, 0x104) => "US") + dcmMG[(0x0008, 0x2218)][1][(0x0008, 0x0104)] = 0x0001 + dcmMG[(0x0054, 0x0220)][1][(0x0008, 0x0104)] = 0x0002 + + outMG3 = joinpath(data_folder, "outMG3.dcm") + dcm_write(outMG3, dcmMG; aux_vr = codemeaning_vr) + dcmMG3 = dcm_parse(outMG3) + @test dcmMG3[(0x0008, 0x2218)][1][(0x0008, 0x0104)] == 0x0001 + @test dcmMG3[(0x0054, 0x0220)][1][(0x0008, 0x0104)] == 0x0002 +end + +function write_to_string(writef) + io = IOBuffer() + writef(io) + return String(take!(io)) +end + +@testset "Padding string values" begin + empty_vr_dict = Dict{Tuple{UInt16, UInt16}, String}() + # Test that UI strings are padded with '\0' to even length + SOPClassUID = write_to_string(io -> DICOM.write_element(io, (0x0008, 0x0016), "1.2.840.10008.5.1.4.1.1.4", true, empty_vr_dict)) + @test length(SOPClassUID) % 2 == 0 + @test SOPClassUID[end] == '\0' + # Test that SH strings are padded with ' ' to even length + StudyDescription = write_to_string(io -> DICOM.write_element(io, (0x0008, 0x1030), "BestImageEver", true, empty_vr_dict)) + @test length(StudyDescription) % 2 == 0 + @test StudyDescription[end] == ' ' +end + +@testset "Writing Sequence" begin + empty_vr_dict = Dict{Tuple{UInt16, UInt16}, String}() + # Setup internal SQ DICOMData + sq_meta = Dict{Tuple{UInt16, UInt16}, Any}([(0x0008, 0x0100) => "UNDEFINED", (0x0008, 0x010b) => 'N', (0x0008, 0x0104) => "UNDEFINED"]) + sq_el = DICOM.DICOMData(sq_meta, :little, true, empty_vr_dict) + + # Setup external SQ DICOMData + meta = Dict{Tuple{UInt16, UInt16}, Any}((0x0040, 0x0260) => [sq_el]) + el = DICOM.DICOMData(meta, :little, true, empty_vr_dict) + + PerformedProtocolSequence = write_to_string(io -> DICOM.write_element(io, (0x0040, 0x0260), el[(0x0040, 0x0260)], true, empty_vr_dict)) + # Check that the length is (0xffffffff) for undefined length SQ + @test PerformedProtocolSequence[9:12] == "\xff\xff\xff\xff" + @test PerformedProtocolSequence[end-7:end] == "\xfe\xff\xdd\xe0\x00\x00\x00\x00" +end