diff --git a/dataset.go b/dataset.go index 4d8a5cb7..ae7b376d 100644 --- a/dataset.go +++ b/dataset.go @@ -36,7 +36,7 @@ func (d *Dataset) FindElementByTag(tag tag.Tag) (*Element, error) { return nil, ErrorElementNotFound } -func (d *Dataset) transferSyntax() (binary.ByteOrder, bool, error) { +func (d *Dataset) TransferSyntax() (binary.ByteOrder, bool, error) { elem, err := d.FindElementByTag(tag.TransferSyntaxUID) if err != nil { return nil, false, err diff --git a/pkg/frame/inplace/pixeldata_info.go b/pkg/frame/inplace/pixeldata_info.go new file mode 100644 index 00000000..df1f612b --- /dev/null +++ b/pkg/frame/inplace/pixeldata_info.go @@ -0,0 +1,143 @@ +package inplace + +import ( + "encoding/binary" + "errors" + "fmt" + "strconv" + + "github.com/suyashkumar/dicom" + "github.com/suyashkumar/dicom/pkg/tag" +) + +// GetIntFromValue is similar to dicom.MustGetInts but without panic +func GetIntFromValue(v dicom.Value) (int, error) { + if v.ValueType() != dicom.Ints { + return 0, errors.New("value is not ints") + } + arr, ok := v.GetValue().([]int) + if !ok { + return 0, errors.New("actual value is not ints") + } + if len(arr) == 0 { + return 0, errors.New("empty ints value") + } + return arr[0], nil +} + +// GetStringFromValue is similar to dicom.MustGetStrings but without panic +func GetStringFromValue(v dicom.Value) (string, error) { + if v.ValueType() != dicom.Strings { + return "", errors.New("value is not ints") + } + arr, ok := v.GetValue().([]string) + if !ok { + return "", errors.New("actual value is not ints") + } + if len(arr) == 0 { + return "", errors.New("empty ints value") + } + return arr[0], nil +} + +// PixelDataMetadata is the metadata for tag.PixelData +type PixelDataMetadata struct { + Rows int + Cols int + Frames int + SamplesPerPixel int + BitsAllocated int + PlanarConfiguration int + Bo binary.ByteOrder +} + +// GetPixelDataMetadata returns the pixel data metadata. +func GetPixelDataMetadata(ds *dicom.Dataset) (*PixelDataMetadata, error) { + re := &PixelDataMetadata{} + rows, err := ds.FindElementByTag(tag.Rows) + if err != nil { + return nil, fmt.Errorf("get Rows element: %w", err) + } + if re.Rows, err = GetIntFromValue(rows.Value); err != nil { + return nil, fmt.Errorf("convert Rows element to int: %w", err) + } + + cols, err := ds.FindElementByTag(tag.Columns) + if err != nil { + return nil, fmt.Errorf("get Columns element: %w", err) + } + if re.Cols, err = GetIntFromValue(cols.Value); err != nil { + return nil, fmt.Errorf("convert Columns element to int: %w", err) + } + + numberOfFrames, err := ds.FindElementByTag(tag.NumberOfFrames) + if err != nil { + re.Frames = 1 + } else { + var framesStr string + framesStr, err = GetStringFromValue(numberOfFrames.Value) + if err != nil { + return nil, fmt.Errorf("convert NumberOfFrames element to str: %w", err) + } + if re.Frames, err = strconv.Atoi(framesStr); err != nil { + return nil, fmt.Errorf("convert NumberOfFrames to int: %w", err) + } + } + + samplesPerPixel, err := ds.FindElementByTag(tag.SamplesPerPixel) + if err != nil { + return nil, fmt.Errorf("get SamplesPerPixel element: %w", err) + } + if re.SamplesPerPixel, err = GetIntFromValue(samplesPerPixel.Value); err != nil { + return nil, fmt.Errorf("convert SamplesPerPixel element to int: %w", err) + } + bitsAllocated, err := ds.FindElementByTag(tag.BitsAllocated) + if err != nil { + return nil, fmt.Errorf("get BitsAllocated element: %w", err) + } + if re.BitsAllocated, err = GetIntFromValue(bitsAllocated.Value); err != nil { + return nil, fmt.Errorf("convert BitsAllocated element to int: %w", err) + } + re.Bo, _, err = ds.TransferSyntax() + if err != nil { + return nil, fmt.Errorf("get byteOrder: %w", err) + } + + planarConfElement, err := ds.FindElementByTag(tag.PlanarConfiguration) + if err != nil { + re.PlanarConfiguration = 0 + } else { + if re.PlanarConfiguration, err = GetIntFromValue(planarConfElement.Value); err != nil { + return nil, fmt.Errorf("convert Rows element to int: %w", err) + } + } + + return re, nil +} + +// IsSafeForUnprocessedValueDataHandling check if we can support in-place read-write +// from Pixeldata.UnprocessedValueData +// This avoids the case that we can not handle it, yet. +func IsSafeForUnprocessedValueDataHandling(info *PixelDataMetadata, unprocessedValueData []byte) error { + // https://dicom.innolitics.com/ciods/enhanced-mr-image/enhanced-mr-image/00280006 + if info.PlanarConfiguration == 1 { + return fmt.Errorf("unsupported PlanarConfiguration: %d", info.PlanarConfiguration) + } + // TODO: support for BitsAllocated == 1 + switch info.BitsAllocated { + case 8, 16, 32: + default: // bitsAllocated = 1 and other cases + return fmt.Errorf("unsupported bit allocated: %d", info.BitsAllocated) + } + pixelsPerFrame := info.Rows * info.Cols + bytesAllocated := info.BitsAllocated / 8 + expectedBytes := bytesAllocated * info.SamplesPerPixel * info.Frames * pixelsPerFrame + // odd number of bytes. + if expectedBytes%2 == 1 { + expectedBytes += 1 + } + if len(unprocessedValueData) != expectedBytes { + return errors.New("mismatch data size") + } + return nil +} diff --git a/pkg/frame/inplace/read.go b/pkg/frame/inplace/read.go new file mode 100644 index 00000000..871ea4e9 --- /dev/null +++ b/pkg/frame/inplace/read.go @@ -0,0 +1,29 @@ +// Package inplace contains code for handling UnprocessedValueData +package inplace + +// ReadUnprocessedValueData read the value of Dicom image directly +// from the byte array of PixelData.UnprocessedValueData with given frame ID. +// This ease the memory usage of reading DICOM image. +func ReadUnprocessedValueData(info *PixelDataMetadata, unprocessedValueData []byte, frameIndex int) ([][]int, error) { + pixelsPerFrame := info.Rows * info.Cols + bytesAllocated := info.BitsAllocated / 8 + offset := frameIndex * pixelsPerFrame * info.SamplesPerPixel * bytesAllocated + samplesPerPixel := info.SamplesPerPixel + + re := make([][]int, samplesPerPixel) + for i := 0; i < samplesPerPixel; i++ { + re[i] = make([]int, pixelsPerFrame) + for j := 0; j < pixelsPerFrame; j++ { + pointOffset := offset + j*info.SamplesPerPixel*bytesAllocated + i*bytesAllocated + switch bytesAllocated { + case 1: + re[i][j] = int(unprocessedValueData[pointOffset]) + case 2: + re[i][j] = int(info.Bo.Uint16(unprocessedValueData[pointOffset : pointOffset+bytesAllocated])) + case 4: + re[i][j] = int(info.Bo.Uint32(unprocessedValueData[pointOffset : pointOffset+bytesAllocated])) + } + } + } + return re, nil +} diff --git a/pkg/frame/inplace/read_test.go b/pkg/frame/inplace/read_test.go new file mode 100644 index 00000000..1461047c --- /dev/null +++ b/pkg/frame/inplace/read_test.go @@ -0,0 +1,257 @@ +package inplace + +import ( + "bytes" + "fmt" + "io" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/suyashkumar/dicom" + "github.com/suyashkumar/dicom/pkg/frame" + "github.com/suyashkumar/dicom/pkg/tag" + "github.com/suyashkumar/dicom/pkg/uid" +) + +func mustNewElement(t *testing.T, tag tag.Tag, data interface{}) *dicom.Element { + t.Helper() + elem, err := dicom.NewElement(tag, data) + if err != nil { + t.Errorf("dicom.NewElement: %v", err) + } + return elem +} + +func TestReadReadUnprocessedValueData(t *testing.T) { + cases := []struct { + Name string + existingData dicom.Dataset + }{ + { + Name: "5x5_1_frame_1_samples/pixel", + existingData: dicom.Dataset{Elements: []*dicom.Element{ + mustNewElement(t, tag.MediaStorageSOPClassUID, []string{"1.2.840.10008.5.1.4.1.1.1.2"}), + mustNewElement(t, tag.MediaStorageSOPInstanceUID, []string{"1.2.3.4.5.6.7"}), + mustNewElement(t, tag.TransferSyntaxUID, []string{uid.ImplicitVRLittleEndian}), + mustNewElement(t, tag.Rows, []int{5}), + mustNewElement(t, tag.Columns, []int{5}), + mustNewElement(t, tag.NumberOfFrames, []string{"1"}), + mustNewElement(t, tag.BitsAllocated, []int{16}), + mustNewElement(t, tag.SamplesPerPixel, []int{1}), + mustNewElement(t, tag.PixelData, dicom.PixelDataInfo{ + IsEncapsulated: false, + Frames: []*frame.Frame{ + { + Encapsulated: false, + NativeData: frame.NativeFrame{ + BitsPerSample: 16, + Rows: 5, + Cols: 5, + Data: [][]int{ + {1}, {2}, {3}, {4}, {5}, + {0}, {0}, {0}, {0}, {0}, + {0}, {0}, {0}, {0}, {0}, + {0}, {0}, {0}, {0}, {0}, + {0}, {0}, {0}, {0}, {0}, + }, + }, + }, + }, + }), + }}, + }, + { + Name: "2x2, 3 frames, 1 samples/pixel", + existingData: dicom.Dataset{ + Elements: []*dicom.Element{ + mustNewElement(t, tag.MediaStorageSOPClassUID, []string{"1.2.840.10008.5.1.4.1.1.1.2"}), + mustNewElement(t, tag.MediaStorageSOPInstanceUID, []string{"1.2.3.4.5.6.7"}), + mustNewElement(t, tag.TransferSyntaxUID, []string{uid.ImplicitVRLittleEndian}), + mustNewElement(t, tag.Rows, []int{2}), + mustNewElement(t, tag.Columns, []int{2}), + mustNewElement(t, tag.NumberOfFrames, []string{"3"}), + mustNewElement(t, tag.BitsAllocated, []int{16}), + mustNewElement(t, tag.SamplesPerPixel, []int{1}), + mustNewElement(t, tag.PixelData, dicom.PixelDataInfo{ + IsEncapsulated: false, + Frames: []*frame.Frame{ + { + Encapsulated: false, + NativeData: frame.NativeFrame{ + BitsPerSample: 16, + Rows: 2, + Cols: 2, + Data: [][]int{{4}, {5}, {6}, {7}}, + }, + }, + { + Encapsulated: false, + NativeData: frame.NativeFrame{ + BitsPerSample: 16, + Rows: 2, + Cols: 2, + Data: [][]int{{1}, {2}, {3}, {2}}, + }, + }, + { + Encapsulated: false, + NativeData: frame.NativeFrame{ + BitsPerSample: 16, + Rows: 2, + Cols: 2, + Data: [][]int{{1}, {2}, {3}, {0}}, + }, + }, + }, + }), + }, + }, + }, + { + Name: "2x2, 2 frames, 2 samples/pixel", + existingData: dicom.Dataset{ + Elements: []*dicom.Element{ + mustNewElement(t, tag.MediaStorageSOPClassUID, []string{"1.2.840.10008.5.1.4.1.1.1.2"}), + mustNewElement(t, tag.MediaStorageSOPInstanceUID, []string{"1.2.3.4.5.6.7"}), + mustNewElement(t, tag.TransferSyntaxUID, []string{uid.ImplicitVRLittleEndian}), + mustNewElement(t, tag.Rows, []int{2}), + mustNewElement(t, tag.Columns, []int{2}), + mustNewElement(t, tag.NumberOfFrames, []string{"2"}), + mustNewElement(t, tag.BitsAllocated, []int{16}), + mustNewElement(t, tag.SamplesPerPixel, []int{2}), + + mustNewElement(t, tag.PixelData, dicom.PixelDataInfo{ + IsEncapsulated: false, + Frames: []*frame.Frame{ + { + Encapsulated: false, + NativeData: frame.NativeFrame{ + BitsPerSample: 16, + Rows: 2, + Cols: 2, + Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 2}}, + }, + }, + { + Encapsulated: false, + NativeData: frame.NativeFrame{ + BitsPerSample: 16, + Rows: 2, + Cols: 2, + Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 5}}, + }, + }, + }, + }), + }, + }, + }, + { + Name: "1x1, 3 frames, 3 samples/pixel, data bytes with padded 0", + existingData: dicom.Dataset{ + Elements: []*dicom.Element{ + mustNewElement(t, tag.MediaStorageSOPClassUID, []string{"1.2.840.10008.5.1.4.1.1.1.2"}), + mustNewElement(t, tag.MediaStorageSOPInstanceUID, []string{"1.2.3.4.5.6.7"}), + mustNewElement(t, tag.TransferSyntaxUID, []string{uid.ImplicitVRLittleEndian}), + mustNewElement(t, tag.Rows, []int{1}), + mustNewElement(t, tag.Columns, []int{1}), + mustNewElement(t, tag.NumberOfFrames, []string{"3"}), + mustNewElement(t, tag.BitsAllocated, []int{8}), + mustNewElement(t, tag.SamplesPerPixel, []int{3}), + + mustNewElement(t, tag.PixelData, dicom.PixelDataInfo{ + IsEncapsulated: false, + Frames: []*frame.Frame{ + { + Encapsulated: false, + NativeData: frame.NativeFrame{ + BitsPerSample: 8, Rows: 1, Cols: 1, + Data: [][]int{{1, 2, 3}}, + }, + }, { + Encapsulated: false, + NativeData: frame.NativeFrame{ + BitsPerSample: 8, Rows: 1, Cols: 1, + Data: [][]int{{4, 5, 6}}, + }, + }, { + Encapsulated: false, + NativeData: frame.NativeFrame{ + BitsPerSample: 8, Rows: 1, Cols: 1, + Data: [][]int{{7, 8, 9}}, + }, + }, + }, + }), + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + var filesOut bytes.Buffer + var err error + if err = dicom.Write(io.Writer(&filesOut), tc.existingData); err != nil { + t.Errorf("Write DICOM obj to bytes: %v", err) + } + rawData := filesOut.Bytes() + + dataset, err := dicom.Parse(bytes.NewReader(rawData), int64(len(rawData)), nil, dicom.SkipProcessingPixelDataValue()) + if err != nil { + t.Errorf("dicom.Parse: %v", err) + } + metadata, err := GetPixelDataMetadata(&dataset) + if err != nil { + t.Errorf("GetPixelDataMetadata: %v", err) + } + pixelElement, err := dataset.FindElementByTag(tag.PixelData) + if err != nil { + t.Errorf("tag.PixelData not found: %v", err) + } + pixelDataInfo := dicom.MustGetPixelDataInfo(pixelElement.Value) + if pixelDataInfo.IsEncapsulated || !pixelDataInfo.IntentionallyUnprocessed { + t.Errorf("pixelDataInfo should be IntentionallyUnprocessed: IsEncapsulated=%t IntentionallyUnprocessed%t", + pixelDataInfo.IsEncapsulated, pixelDataInfo.IntentionallyUnprocessed) + } + + if err = IsSafeForUnprocessedValueDataHandling(metadata, pixelDataInfo.UnprocessedValueData); err != nil { + t.Errorf("IsSafeForUnprocessedValueDataHandling(%v)", err) + } + + originPixelElement, err := tc.existingData.FindElementByTag(tag.PixelData) + if err != nil { + t.Errorf("Find tag.PixelData(%v)", err) + } + originPixelDataInfo := dicom.MustGetPixelDataInfo(originPixelElement.Value) + + for i := 0; i < metadata.Frames; i++ { + originData := originPixelDataInfo.Frames[i].NativeData.Data + inplaceData, err := ReadUnprocessedValueData(metadata, pixelDataInfo.UnprocessedValueData, i) + if err != nil { + t.Errorf("ReadUnprocessedValueData(%v)", err) + } + assertUnprocessedPixelData(t, originData, inplaceData, fmt.Sprint("frame:", i)) + } + }) + } +} + +func assertUnprocessedPixelData(t *testing.T, inplaceData [][]int, originData [][]int, msg string) { + if len(inplaceData[0]) != len(originData) || len(inplaceData) != len(originData[0]) { + t.Errorf("Mismatch data size: origin-data(%d-%d) inplace-data(%d-%d)", + len(originData), len(originData[0]), len(inplaceData), len(inplaceData[0])) + } + tmp := make([][]int, len(originData)) + for i := 0; i < len(tmp); i++ { + tmp[i] = make([]int, len(originData[0])) + for j := 0; j < len(originData[0]); j++ { + tmp[i][j] = inplaceData[j][i] + } + } + if diff := cmp.Diff(originData, tmp, cmpopts.EquateErrors()); diff != "" { + t.Errorf("assertUnprocessedPixelData(%v-%s): unexpected diff: %v", inplaceData, msg, diff) + } +} diff --git a/pkg/frame/inplace/write.go b/pkg/frame/inplace/write.go new file mode 100644 index 00000000..dcd7b5a4 --- /dev/null +++ b/pkg/frame/inplace/write.go @@ -0,0 +1,39 @@ +package inplace + +import ( + "bytes" + "encoding/binary" +) + +// WriteUnprocessedValueData write the value of Dicom image directly to +// byte array of PixelData.UnprocessedValueData +func WriteUnprocessedValueData(info *PixelDataMetadata, unprocessedValueData []byte, + rowIndex, colIndex int, frameIndex int, sampleIndex int, newValue int) error { + pixelsPerFrame := info.Rows * info.Cols + bytesAllocated := info.BitsAllocated / 8 + buf := &bytes.Buffer{} + buf.Grow(bytesAllocated) + switch bytesAllocated { + case 1: + if err := binary.Write(buf, info.Bo, uint8(newValue)); err != nil { + return err + } + case 2: + if err := binary.Write(buf, info.Bo, uint16(newValue)); err != nil { + return err + } + case 4: + if err := binary.Write(buf, info.Bo, uint32(newValue)); err != nil { + return err + } + } + paintedPixel := buf.Bytes() + offset := bytesAllocated * info.SamplesPerPixel * frameIndex * pixelsPerFrame + offset += info.SamplesPerPixel * bytesAllocated * (rowIndex*info.Cols + colIndex) + + offset += bytesAllocated * sampleIndex + for k := 0; k < bytesAllocated; k++ { + unprocessedValueData[offset+k] = paintedPixel[k] + } + return nil +} diff --git a/pkg/frame/inplace/write_test.go b/pkg/frame/inplace/write_test.go new file mode 100644 index 00000000..6b81f400 --- /dev/null +++ b/pkg/frame/inplace/write_test.go @@ -0,0 +1,255 @@ +package inplace + +import ( + "bytes" + "io" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/suyashkumar/dicom" + "github.com/suyashkumar/dicom/pkg/frame" + "github.com/suyashkumar/dicom/pkg/tag" + "github.com/suyashkumar/dicom/pkg/uid" +) + +func TestWriteUnprocessedValueData(t *testing.T) { + originDataset := dicom.Dataset{ + Elements: []*dicom.Element{ + mustNewElement(t, tag.MediaStorageSOPClassUID, []string{"1.2.840.10008.5.1.4.1.1.1.2"}), + mustNewElement(t, tag.MediaStorageSOPInstanceUID, []string{"1.2.3.4.5.6.7"}), + mustNewElement(t, tag.TransferSyntaxUID, []string{uid.ImplicitVRLittleEndian}), + mustNewElement(t, tag.Rows, []int{2}), + mustNewElement(t, tag.Columns, []int{2}), + mustNewElement(t, tag.NumberOfFrames, []string{"2"}), + mustNewElement(t, tag.BitsAllocated, []int{16}), + mustNewElement(t, tag.SamplesPerPixel, []int{2}), + + mustNewElement(t, tag.PixelData, dicom.PixelDataInfo{ + IsEncapsulated: false, + Frames: []*frame.Frame{ + { + Encapsulated: false, + NativeData: frame.NativeFrame{ + BitsPerSample: 16, + Rows: 2, + Cols: 2, + Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 2}}, + }, + }, + { + Encapsulated: false, + NativeData: frame.NativeFrame{ + BitsPerSample: 16, + Rows: 2, + Cols: 2, + Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 5}}, + }, + }, + }, + }), + }, + } + var buffer bytes.Buffer + if err := dicom.Write(io.Writer(&buffer), originDataset); err != nil { + t.Errorf("write from dataset to DICOM bytes: %v", err) + } + rawData := buffer.Bytes() + + for _, tc := range []struct { + sampleIndex, rowIndex, colIndex, frameIndex int + expectedPixelInfo dicom.PixelDataInfo + }{ + { + sampleIndex: 0, colIndex: 1, rowIndex: 0, frameIndex: 0, + expectedPixelInfo: dicom.PixelDataInfo{ + Frames: []*frame.Frame{ + {NativeData: frame.NativeFrame{Data: [][]int{{1, 2}, {100, 2}, {1, 2}, {3, 2}}, BitsPerSample: 16, Rows: 2, Cols: 2}}, + {NativeData: frame.NativeFrame{Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 5}}, BitsPerSample: 16, Rows: 2, Cols: 2}}, + }, + }, + }, + { + sampleIndex: 1, colIndex: 1, rowIndex: 0, frameIndex: 0, + expectedPixelInfo: dicom.PixelDataInfo{ + Frames: []*frame.Frame{ + {NativeData: frame.NativeFrame{Data: [][]int{{1, 2}, {3, 100}, {1, 2}, {3, 2}}, BitsPerSample: 16, Rows: 2, Cols: 2}}, + {NativeData: frame.NativeFrame{Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 5}}, BitsPerSample: 16, Rows: 2, Cols: 2}}, + }, + }, + }, + { + sampleIndex: 1, colIndex: 0, rowIndex: 1, frameIndex: 0, + expectedPixelInfo: dicom.PixelDataInfo{ + Frames: []*frame.Frame{ + {NativeData: frame.NativeFrame{Data: [][]int{{1, 2}, {3, 2}, {1, 100}, {3, 2}}, BitsPerSample: 16, Rows: 2, Cols: 2}}, + {NativeData: frame.NativeFrame{Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 5}}, BitsPerSample: 16, Rows: 2, Cols: 2}}, + }, + }, + }, + { + sampleIndex: 1, colIndex: 0, rowIndex: 1, frameIndex: 1, + expectedPixelInfo: dicom.PixelDataInfo{ + Frames: []*frame.Frame{ + {NativeData: frame.NativeFrame{Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 2}}, BitsPerSample: 16, Rows: 2, Cols: 2}}, + {NativeData: frame.NativeFrame{Data: [][]int{{1, 2}, {3, 2}, {1, 100}, {3, 5}}, BitsPerSample: 16, Rows: 2, Cols: 2}}, + }, + }, + }, + } { + var err error + dataset, metadata, pixelDataInfo := setupUnprocessedPixelData(t, rawData) + // in-place write then converts to bytes + if err = WriteUnprocessedValueData(metadata, pixelDataInfo.UnprocessedValueData, + tc.rowIndex, tc.colIndex, tc.frameIndex, tc.sampleIndex, 100); err != nil { + t.Errorf("WriteUnprocessedValueData: %v", err) + } + var buffer2 bytes.Buffer + if err = dicom.Write(io.Writer(&buffer2), dataset); err != nil { + t.Errorf("Write new DICOM obj to file: %v", err) + } + outputBytes := buffer2.Bytes() + assertPixelDataInfo(t, outputBytes, tc.expectedPixelInfo) + } +} + +func TestWriteUnprocessedValueData_BigEndian(t *testing.T) { + originDataset := dicom.Dataset{ + Elements: []*dicom.Element{ + mustNewElement(t, tag.MediaStorageSOPClassUID, []string{"1.2.840.10008.5.1.4.1.1.1.2"}), + mustNewElement(t, tag.MediaStorageSOPInstanceUID, []string{"1.2.3.4.5.6.7"}), + mustNewElement(t, tag.TransferSyntaxUID, []string{uid.ExplicitVRBigEndian}), + mustNewElement(t, tag.Rows, []int{2}), + mustNewElement(t, tag.Columns, []int{2}), + mustNewElement(t, tag.NumberOfFrames, []string{"2"}), + mustNewElement(t, tag.BitsAllocated, []int{16}), + mustNewElement(t, tag.SamplesPerPixel, []int{2}), + + mustNewElement(t, tag.PixelData, dicom.PixelDataInfo{ + IsEncapsulated: false, + Frames: []*frame.Frame{ + { + Encapsulated: false, + NativeData: frame.NativeFrame{ + BitsPerSample: 16, + Rows: 2, + Cols: 2, + Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 2}}, + }, + }, + { + Encapsulated: false, + NativeData: frame.NativeFrame{ + BitsPerSample: 16, + Rows: 2, + Cols: 2, + Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 5}}, + }, + }, + }, + }), + }, + } + var buffer bytes.Buffer + if err := dicom.Write(io.Writer(&buffer), originDataset); err != nil { + t.Errorf("write from dataset to DICOM bytes: %v", err) + } + rawData := buffer.Bytes() + + for _, tc := range []struct { + sampleIndex, rowIndex, colIndex, frameIndex int + expectedPixelInfo dicom.PixelDataInfo + }{ + { + sampleIndex: 0, colIndex: 1, rowIndex: 0, frameIndex: 0, + expectedPixelInfo: dicom.PixelDataInfo{ + Frames: []*frame.Frame{ + {NativeData: frame.NativeFrame{Data: [][]int{{1, 2}, {100, 2}, {1, 2}, {3, 2}}, BitsPerSample: 16, Rows: 2, Cols: 2}}, + {NativeData: frame.NativeFrame{Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 5}}, BitsPerSample: 16, Rows: 2, Cols: 2}}, + }, + }, + }, + { + sampleIndex: 1, colIndex: 1, rowIndex: 0, frameIndex: 0, + expectedPixelInfo: dicom.PixelDataInfo{ + Frames: []*frame.Frame{ + {NativeData: frame.NativeFrame{Data: [][]int{{1, 2}, {3, 100}, {1, 2}, {3, 2}}, BitsPerSample: 16, Rows: 2, Cols: 2}}, + {NativeData: frame.NativeFrame{Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 5}}, BitsPerSample: 16, Rows: 2, Cols: 2}}, + }, + }, + }, + { + sampleIndex: 1, colIndex: 0, rowIndex: 1, frameIndex: 0, + expectedPixelInfo: dicom.PixelDataInfo{ + Frames: []*frame.Frame{ + {NativeData: frame.NativeFrame{Data: [][]int{{1, 2}, {3, 2}, {1, 100}, {3, 2}}, BitsPerSample: 16, Rows: 2, Cols: 2}}, + {NativeData: frame.NativeFrame{Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 5}}, BitsPerSample: 16, Rows: 2, Cols: 2}}, + }, + }, + }, + { + sampleIndex: 1, colIndex: 0, rowIndex: 1, frameIndex: 1, + expectedPixelInfo: dicom.PixelDataInfo{ + Frames: []*frame.Frame{ + {NativeData: frame.NativeFrame{Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 2}}, BitsPerSample: 16, Rows: 2, Cols: 2}}, + {NativeData: frame.NativeFrame{Data: [][]int{{1, 2}, {3, 2}, {1, 100}, {3, 5}}, BitsPerSample: 16, Rows: 2, Cols: 2}}, + }, + }, + }, + } { + dataset, metadata, pixelDataInfo := setupUnprocessedPixelData(t, rawData) + var err error + // in-place write then converts to bytes + if err = WriteUnprocessedValueData(metadata, pixelDataInfo.UnprocessedValueData, + tc.rowIndex, tc.colIndex, tc.frameIndex, tc.sampleIndex, 100); err != nil { + t.Errorf("WriteUnprocessedValueData: %v", err) + } + var buffer2 bytes.Buffer + if err = dicom.Write(io.Writer(&buffer2), dataset); err != nil { + t.Errorf("Write new DICOM obj to file: %v", err) + } + outputBytes := buffer2.Bytes() + assertPixelDataInfo(t, outputBytes, tc.expectedPixelInfo) + } +} + +func setupUnprocessedPixelData(t testing.TB, rawData []byte) (dicom.Dataset, *PixelDataMetadata, dicom.PixelDataInfo) { + dataset, err := dicom.Parse(bytes.NewReader(rawData), int64(len(rawData)), nil, dicom.SkipProcessingPixelDataValue()) + if err != nil { + t.Errorf("dicom.Parse: %v", err) + } + metadata, err := GetPixelDataMetadata(&dataset) + if err != nil { + t.Errorf("GetPixelDataMetadata: %v", err) + } + pixelElement, err := dataset.FindElementByTag(tag.PixelData) + if err != nil { + t.Errorf("tag.PixelData not found: %v", err) + } + pixelDataInfo := dicom.MustGetPixelDataInfo(pixelElement.Value) + if !pixelDataInfo.IntentionallyUnprocessed || pixelDataInfo.IsEncapsulated { + t.Errorf("unexpected pixelDataInfo: IntentionallyUnprocessed=%t IsEncapsulated=%t", + pixelDataInfo.IntentionallyUnprocessed, pixelDataInfo.IsEncapsulated) + } + // Sanity check if the dataset is suitable for in-place read-write + if err = IsSafeForUnprocessedValueDataHandling(metadata, pixelDataInfo.UnprocessedValueData); err != nil { + t.Errorf("IsSafeForUnprocessedValueDataHandling(%v)", err) + } + return dataset, metadata, pixelDataInfo +} + +func assertPixelDataInfo(t testing.TB, rawData []byte, expectedPixelDataInfo dicom.PixelDataInfo) { + dataset2, err := dicom.Parse(bytes.NewReader(rawData), int64(len(rawData)), nil) + if err != nil { + t.Errorf("read new create dcm file: %v", err) + } + pixelElement, err := dataset2.FindElementByTag(tag.PixelData) + if err != nil { + t.Errorf("pixel data not found: %v", err) + } + pixelDataInfo := dicom.MustGetPixelDataInfo(pixelElement.Value) + if diff := cmp.Diff(pixelDataInfo, expectedPixelDataInfo, cmpopts.EquateErrors()); diff != "" { + t.Errorf("assert pixelDataInfo in dicom file(%v): unexpected diff: %v", pixelDataInfo, diff) + } +} diff --git a/write.go b/write.go index d58aca1e..b0814c88 100644 --- a/write.go +++ b/write.go @@ -64,7 +64,7 @@ func (w *Writer) writeDataset(ds Dataset) error { return err } - endian, implicit, err := ds.transferSyntax() + endian, implicit, err := ds.TransferSyntax() if (err != nil && err != ErrorElementNotFound) || (err == ErrorElementNotFound && !w.optSet.defaultMissingTransferSyntax) { return err }