From 46895b8f7546eec8e640ad0a29ffca71d8e9e2d0 Mon Sep 17 00:00:00 2001 From: Suyash Kumar Date: Sat, 1 Jun 2024 17:57:11 -0400 Subject: [PATCH] Deflate transfer syntax support (for parse) (#321) This adds support for Deflated Little Endian Explicit transfer syntax on parsing. Support on writing should come in another change. This change does this by adding a one way SetDeflate mechanism to the dicomio.Reader. The expectation for now is that this internal API is called once and at the top level of dicom parsing (e.g this doesn't support parsing just some sub-payloads with deflation, though we can make some changes to support that in the future if it's a need). --- parse.go | 19 ++++++--- pkg/dicomio/reader.go | 17 ++++++++ pkg/uid/uid.go | 2 +- testdata/6.dcm | Bin 0 -> 4637 bytes testdata/data_details.md | 8 ++-- write.go | 6 ++- write_test.go | 88 +++++++++++++++++++++++---------------- 7 files changed, 93 insertions(+), 47 deletions(-) create mode 100644 testdata/6.dcm diff --git a/parse.go b/parse.go index ef15c863..7206072d 100644 --- a/parse.go +++ b/parse.go @@ -158,15 +158,22 @@ func NewParser(in io.Reader, bytesToRead int64, frameChannel chan *frame.Frame, implicit := true ts, err := p.dataset.FindElementByTag(tag.TransferSyntaxUID) - if err != nil { - debug.Log("WARN: could not find transfer syntax uid in metadata, proceeding with little endian implicit") - } else { - bo, implicit, err = uid.ParseTransferSyntaxUID(MustGetStrings(ts.Value)[0]) + if err == nil { + // If we found the transfer syntax, apply it. + tsStr := MustGetStrings(ts.Value)[0] + bo, implicit, err = uid.ParseTransferSyntaxUID(tsStr) if err != nil { // TODO(suyashkumar): should we attempt to parse with LittleEndian // Implicit here? debug.Log("WARN: could not parse transfer syntax uid in metadata") } + if tsStr == uid.DeflatedExplicitVRLittleEndian { + p.reader.rawReader.SetDeflate() + } + } else { + // No transfer syntax found, warn the user we're proceeding with the + // default Little Endian implicit. + debug.Log("WARN: could not find transfer syntax uid in metadata, proceeding with little endian implicit") } p.SetTransferSyntax(bo, implicit) @@ -281,8 +288,8 @@ func SkipPixelData() ParseOption { // a PixelData element will be added to the dataset with the // PixelDataInfo.IntentionallyUnprocessed = true, and the raw bytes of the // entire PixelData element stored in PixelDataInfo.UnprocessedValueData. -// -// In the future, we may be able to extend this functionality to support +// +// In the future, we may be able to extend this functionality to support // on-demand processing of elements elsewhere in the library. func SkipProcessingPixelDataValue() ParseOption { return func(set *parseOptSet) { diff --git a/pkg/dicomio/reader.go b/pkg/dicomio/reader.go index fe683ef8..605b1057 100644 --- a/pkg/dicomio/reader.go +++ b/pkg/dicomio/reader.go @@ -2,6 +2,7 @@ package dicomio import ( "bufio" + "compress/flate" "encoding/binary" "errors" "fmt" @@ -71,7 +72,16 @@ type Reader interface { // SetCodingSystem sets the charset.CodingSystem to be used when ReadString // is called. SetCodingSystem(cs charset.CodingSystem) + // ByteOrder returns the current byte order. ByteOrder() binary.ByteOrder + // SetDeflate applies deflate decompression to the underlying reader for all + // subsequent reads. This should be set when working with a deflated + // transfer syntax. Right now this is expected to be called once when + // parsing the top level dicom data, and there is no facility to swap + // between deflate and non-deflate reading. + // This also sets the current limit to LimitReadUntilEOF, since the original + // limits (if any) will be based on uncompressed bytes. + SetDeflate() } type reader struct { @@ -234,6 +244,13 @@ func (r *reader) SetTransferSyntax(bo binary.ByteOrder, implicit bool) { r.implicit = implicit } +func (r *reader) SetDeflate() { + r.in = bufio.NewReader(flate.NewReader(r.in)) + // TODO(https://github.com/suyashkumar/dicom/issues/320): consider always + // having the top level limit read until EOF. + r.limit = LimitReadUntilEOF // needed because original limits may not apply to the deflated reader +} + func (r *reader) IsImplicit() bool { return r.implicit } func (r *reader) SetCodingSystem(cs charset.CodingSystem) { diff --git a/pkg/uid/uid.go b/pkg/uid/uid.go index cd21562d..f0eff51f 100644 --- a/pkg/uid/uid.go +++ b/pkg/uid/uid.go @@ -56,7 +56,7 @@ func ParseTransferSyntaxUID(uid string) (bo binary.ByteOrder, implicit bool, err case ImplicitVRLittleEndian: return binary.LittleEndian, true, nil case DeflatedExplicitVRLittleEndian: - fallthrough + return binary.LittleEndian, false, nil case ExplicitVRLittleEndian: return binary.LittleEndian, false, nil case ExplicitVRBigEndian: diff --git a/testdata/6.dcm b/testdata/6.dcm new file mode 100644 index 0000000000000000000000000000000000000000..eaf2826f295a378f573fd24a9502dbac381756fb GIT binary patch literal 4637 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 O0;?ac51ZmV)ahR<`@9DL literal 0 HcmV?d00001 diff --git a/testdata/data_details.md b/testdata/data_details.md index 40c37d48..7247df43 100644 --- a/testdata/data_details.md +++ b/testdata/data_details.md @@ -32,6 +32,8 @@ be mentioned in one of them for brevity. * Modality: CT * Multiple frames * Native pixel data +* [6.dcm](6.dcm) + * Deflated Little Endian Transfer Syntax (Explicit VR) ### Relevant Citations #### For files 1.dcm, 2.dcm: ##### Data Citation: @@ -58,6 +60,6 @@ Desai, S., Baghal, A., Wongsurawat, T., Al-Shukri, S., Gates, K., Farmer, P., Ru #### TCIA Citation Clark K, Vendt B, Smith K, Freymann J, Kirby J, Koppel P, Moore S, Phillips S, Maffitt D, Pringle M, Tarbox L, Prior F. The Cancer Imaging Archive (TCIA): Maintaining and Operating a Public Information Repository, Journal of Digital Imaging, Volume 26, Number 6, December, 2013, pp 1045-1057. DOI: 10.1007/s10278-013-9622-7 -#### File 5.dcm -This file was sourced from [cornerstone](https://github.com/cornerstonejs/dicomParser/blob/master/testImages/encapsulated/multi-frame/CT0012.explicit_little_endian.dcm) -(which is MIT licensed, see the license reproduced in included_licenses.md) +#### File 5.dcm & 6.dcm +This file was sourced from cornerstone [5.dcm from here](https://github.com/cornerstonejs/dicomParser/blob/master/testImages/encapsulated/multi-frame/CT0012.explicit_little_endian.dcm), and [6.dcm from here](https://github.com/cornerstonejs/dicomParser/blob/7d2084349bf2bdaffe74021e27b286a6c295ca66/testImages/deflate/image_dfl). +(Cornerstone is MIT licensed, see the license reproduced in included_licenses.md). diff --git a/write.go b/write.go index bec9a4b9..0da1720e 100644 --- a/write.go +++ b/write.go @@ -26,7 +26,8 @@ var ( ErrorUnexpectedValueType = errors.New("Unexpected ValueType") // ErrorUnsupportedBitsPerSample indicates that the BitsPerSample in this // Dataset is not supported when unpacking native PixelData. - ErrorUnsupportedBitsPerSample = errors.New("unsupported BitsPerSample value") + ErrorUnsupportedBitsPerSample = errors.New("unsupported BitsPerSample value") + errorDeflatedTransferSyntaxUnsupported = errors.New("deflated explicit vr little endian transfer syntax not yet support on write (https://github.com/suyashkumar/dicom/issues/323)") ) // Writer is a struct that allows element-by element writing to a DICOM writer. @@ -261,6 +262,9 @@ func writeMetaElem(w dicomio.Writer, t tag.Tag, ds *Dataset, tagsUsed *map[tag.T if err != nil { return err } + if elem.Tag == tag.TransferSyntaxUID && MustGetStrings(elem.Value)[0] == uid.DeflatedExplicitVRLittleEndian { + return errorDeflatedTransferSyntaxUnsupported + } err = writeElement(w, elem, optSet) if err != nil { return err diff --git a/write_test.go b/write_test.go index a5fc1840..b9384e19 100644 --- a/write_test.go +++ b/write_test.go @@ -3,6 +3,7 @@ package dicom import ( "bytes" "encoding/binary" + "errors" "os" "testing" @@ -27,13 +28,13 @@ import ( // Write implementation (e.g. it kinda goes both ways and covers Parse too). func TestWrite(t *testing.T) { cases := []struct { - name string - dataset Dataset - extraElems []*Element - expectedError error - opts []WriteOption - parseOpts []ParseOption - cmpOpts []cmp.Option + name string + dataset Dataset + extraElems []*Element + wantError error + opts []WriteOption + parseOpts []ParseOption + cmpOpts []cmp.Option }{ { name: "basic types", @@ -58,7 +59,7 @@ func TestWrite(t *testing.T) { }, }, }}, - expectedError: nil, + wantError: nil, }, { name: "private tag", @@ -70,7 +71,7 @@ func TestWrite(t *testing.T) { mustNewElement(tag.TransferSyntaxUID, []string{uid.ExplicitVRLittleEndian}), mustNewPrivateElement(tag.Tag{0x0003, 0x0010}, vrraw.ShortText, []string{"some data"}), }}, - expectedError: nil, + wantError: nil, }, { name: "sequence (2 Items with 2 values each)", @@ -120,7 +121,7 @@ func TestWrite(t *testing.T) { }, }), }}, - expectedError: nil, + wantError: nil, }, { name: "sequence (2 Items with 2 values each) - skip vr verification", @@ -170,8 +171,8 @@ func TestWrite(t *testing.T) { }, }), }}, - expectedError: nil, - opts: []WriteOption{SkipVRVerification()}, + wantError: nil, + opts: []WriteOption{SkipVRVerification()}, }, { name: "nested sequences", @@ -207,7 +208,7 @@ func TestWrite(t *testing.T) { }, }), }}, - expectedError: nil, + wantError: nil, }, { name: "nested sequences - without VR verification", @@ -243,8 +244,8 @@ func TestWrite(t *testing.T) { }, }), }}, - expectedError: nil, - opts: []WriteOption{SkipVRVerification()}, + wantError: nil, + opts: []WriteOption{SkipVRVerification()}, }, { name: "without transfer syntax", @@ -255,7 +256,7 @@ func TestWrite(t *testing.T) { mustNewElement(tag.Rows, []int{128}), mustNewElement(tag.FloatingPointValue, []float64{128.10}), }}, - expectedError: ErrorElementNotFound, + wantError: ErrorElementNotFound, }, { name: "without transfer syntax with DefaultMissingTransferSyntax", @@ -267,9 +268,9 @@ func TestWrite(t *testing.T) { mustNewElement(tag.FloatingPointValue, []float64{128.10}), }}, // This gets inserted if DefaultMissingTransferSyntax is provided: - extraElems: []*Element{mustNewElement(tag.TransferSyntaxUID, []string{uid.ImplicitVRLittleEndian})}, - expectedError: nil, - opts: []WriteOption{DefaultMissingTransferSyntax()}, + extraElems: []*Element{mustNewElement(tag.TransferSyntaxUID, []string{uid.ImplicitVRLittleEndian})}, + wantError: nil, + opts: []WriteOption{DefaultMissingTransferSyntax()}, }, { name: "native PixelData: 8bit", @@ -299,7 +300,7 @@ func TestWrite(t *testing.T) { mustNewElement(tag.FloatingPointValue, []float64{128.10}), mustNewElement(tag.DimensionIndexPointer, []int{32, 36950}), }}, - expectedError: nil, + wantError: nil, }, { name: "native PixelData: 16bit", @@ -327,7 +328,7 @@ func TestWrite(t *testing.T) { }, }), }}, - expectedError: nil, + wantError: nil, }, { name: "native PixelData: 32bit", @@ -355,7 +356,7 @@ func TestWrite(t *testing.T) { }, }), }}, - expectedError: nil, + wantError: nil, }, { name: "native PixelData: 2 SamplesPerPixel, 2 frames", @@ -392,7 +393,7 @@ func TestWrite(t *testing.T) { }, }), }}, - expectedError: nil, + wantError: nil, }, { name: "encapsulated PixelData", @@ -419,7 +420,7 @@ func TestWrite(t *testing.T) { mustNewElement(tag.FloatingPointValue, []float64{128.10}), mustNewElement(tag.DimensionIndexPointer, []int{32, 36950}), }}, - expectedError: nil, + wantError: nil, }, { name: "encapsulated PixelData: multiframe", @@ -444,7 +445,7 @@ func TestWrite(t *testing.T) { mustNewElement(tag.FloatingPointValue, []float64{128.10}), mustNewElement(tag.DimensionIndexPointer, []int{32, 36950}), }}, - expectedError: nil, + wantError: nil, }, { name: "native_PixelData_2samples_2frames_BigEndian", @@ -481,7 +482,7 @@ func TestWrite(t *testing.T) { }, }), }}, - expectedError: nil, + wantError: nil, }, { name: "native_PixelData_odd_bytes", @@ -509,7 +510,7 @@ func TestWrite(t *testing.T) { }, }), }}, - expectedError: nil, + wantError: nil, }, { name: "PixelData with IntentionallyUnprocessed=true", @@ -526,8 +527,8 @@ func TestWrite(t *testing.T) { IsEncapsulated: false, }), }}, - parseOpts: []ParseOption{SkipProcessingPixelDataValue()}, - expectedError: nil, + parseOpts: []ParseOption{SkipProcessingPixelDataValue()}, + wantError: nil, }, { name: "Native PixelData with IntentionallySkipped=true", @@ -542,8 +543,8 @@ func TestWrite(t *testing.T) { IsEncapsulated: false, }), }}, - parseOpts: []ParseOption{SkipPixelData()}, - expectedError: nil, + parseOpts: []ParseOption{SkipPixelData()}, + wantError: nil, }, { name: "Encapsulated PixelData with IntentionallySkipped=true", @@ -558,8 +559,23 @@ func TestWrite(t *testing.T) { IsEncapsulated: true, })), }}, - parseOpts: []ParseOption{SkipPixelData()}, - expectedError: nil, + parseOpts: []ParseOption{SkipPixelData()}, + wantError: nil, + }, + { + name: "deflated transfer syntax returns error", + dataset: Dataset{Elements: []*Element{ + mustNewElement(tag.MediaStorageSOPClassUID, []string{"1.2.840.10008.5.1.4.1.1.1.2"}), + mustNewElement(tag.MediaStorageSOPInstanceUID, []string{"1.2.3.4.5.6.7"}), + mustNewElement(tag.TransferSyntaxUID, []string{uid.DeflatedExplicitVRLittleEndian}), + mustNewElement(tag.BitsAllocated, []int{8}), + mustNewElement(tag.FloatingPointValue, []float64{128.10}), + setUndefinedLength(mustNewElement(tag.PixelData, PixelDataInfo{ + IntentionallySkipped: true, + IsEncapsulated: true, + })), + }}, + parseOpts: []ParseOption{SkipPixelData()}, wantError: errorDeflatedTransferSyntaxUnsupported, }, } for _, tc := range cases { @@ -568,12 +584,12 @@ func TestWrite(t *testing.T) { if err != nil { t.Fatalf("Unexpected error when creating tempfile: %v", err) } - if err = Write(file, tc.dataset, tc.opts...); err != tc.expectedError { - t.Fatalf("Write(%v): unexpected error. got: %v, want: %v", tc.dataset, err, tc.expectedError) + if err = Write(file, tc.dataset, tc.opts...); !errors.Is(err, tc.wantError) { + t.Fatalf("Write(%v): unexpected error. got: %v, want: %v", tc.dataset, err, tc.wantError) } file.Close() // If we expect an error, we do not need to continue to check the value of the written data, so we continue to the next test case. - if tc.expectedError != nil { + if tc.wantError != nil { return } // Read the data back in and check for equality to the tc.dataset: