diff --git a/Modules/IO/DCMTK/include/itkDCMTKFileReader.h b/Modules/IO/DCMTK/include/itkDCMTKFileReader.h index 299283c1f677..d8e5385ea38b 100644 --- a/Modules/IO/DCMTK/include/itkDCMTKFileReader.h +++ b/Modules/IO/DCMTK/include/itkDCMTKFileReader.h @@ -35,6 +35,7 @@ #include "dcmtk/dcmdata/dcsequen.h" #include "itkMacro.h" #include "itkImageIOBase.h" +#include "itkMetaDataDictionary.h" class DcmSequenceOfItems; class DcmFileFormat; @@ -498,6 +499,9 @@ class ITKIODCMTK_EXPORT DCMTKFileReader static bool IsImageFile(const std::string & filename); + void + PopulateMetaDataDictionary(MetaDataDictionary & dict) const; + private: std::string m_FileName; DcmFileFormat * m_DFile{ nullptr }; diff --git a/Modules/IO/DCMTK/src/itkDCMTKFileReader.cxx b/Modules/IO/DCMTK/src/itkDCMTKFileReader.cxx index c41762f7d9ee..79a855f54742 100644 --- a/Modules/IO/DCMTK/src/itkDCMTKFileReader.cxx +++ b/Modules/IO/DCMTK/src/itkDCMTKFileReader.cxx @@ -33,6 +33,7 @@ #include "dcmtk/dcmdata/dcvrus.h" /* for DcmUnsignedShort */ #include "dcmtk/dcmdata/dcvris.h" /* for DcmIntegerString */ #include "dcmtk/dcmdata/dcvrobow.h" /* for DcmOtherByteOtherWord */ +#include "dcmtk/dcmdata/dcelem.h" /* for DcmElement */ #include "dcmtk/dcmdata/dcvrui.h" /* for DcmUniqueIdentifier */ #include "dcmtk/dcmdata/dcfilefo.h" /* for DcmFileFormat */ #include "dcmtk/dcmdata/dcmetinf.h" /* for DcmMetaInfo */ @@ -47,7 +48,10 @@ #include "vnl/vnl_cross.h" #include "itkIntTypes.h" +#include "itkMetaDataObject.h" +#include "itksys/Base64.h" #include +#include namespace itk { @@ -1369,6 +1373,72 @@ DCMTKFileReader::GetFileNumber() const return m_FileNumber; } +void +DCMTKFileReader::PopulateMetaDataDictionary(MetaDataDictionary & dict) const +{ + dict.Clear(); + if (m_Dataset == nullptr) + { + return; + } + const unsigned long numElements = m_Dataset->card(); + for (unsigned long i = 0; i < numElements; ++i) + { + DcmElement * element = m_Dataset->getElement(i); + if (element == nullptr) + { + continue; + } + const DcmTag & tag = element->getTag(); + // Skip pixel data (7FE0,0010) + if (tag.getGroup() == 0x7fe0 && tag.getElement() == 0x0010) + { + continue; + } + // Format key as "GGGG|EEEE" (lowercase hex, matching GDCMImageIO) + char key[10]; + std::snprintf(key, sizeof(key), "%04x|%04x", tag.getGroup(), tag.getElement()); + + const DcmEVR vr = element->getVR(); + if (vr == EVR_SQ) + { + // Sequences are nested datasets, not byte arrays; getUint8Array() does + // not return their content. Skip rather than emit an empty entry. + continue; + } + if (vr == EVR_OB || vr == EVR_OW || vr == EVR_OF || vr == EVR_OD || vr == EVR_OL || vr == EVR_OV || vr == EVR_UN || + vr == EVR_ox || vr == EVR_px) + { + // Binary VR — base64-encode the raw bytes + Uint8 * byteValue = nullptr; + if (element->getUint8Array(byteValue) == EC_Normal && byteValue != nullptr) + { + const Uint32 length = element->getLength(); + if (length > 0) + { + int encodedLengthEstimate = 2 * static_cast(length); + encodedLengthEstimate = ((encodedLengthEstimate / 4) + 1) * 4; + const auto bin = std::make_unique(encodedLengthEstimate); + const auto encodedLengthActual = + static_cast(itksysBase64_Encode(reinterpret_cast(byteValue), + static_cast(length), + reinterpret_cast(bin.get()), + 0)); + EncapsulateMetaData(dict, key, std::string(bin.get(), encodedLengthActual)); + } + } + } + else + { + OFString value; + if (element->getOFStringArray(value) == EC_Normal) + { + EncapsulateMetaData(dict, key, std::string(value.c_str())); + } + } + } +} + void DCMTKFileReader::AddDictEntry(DcmDictEntry * entry) { diff --git a/Modules/IO/DCMTK/src/itkDCMTKImageIO.cxx b/Modules/IO/DCMTK/src/itkDCMTKImageIO.cxx index d1bb903c382f..1fa5ad708888 100644 --- a/Modules/IO/DCMTK/src/itkDCMTKImageIO.cxx +++ b/Modules/IO/DCMTK/src/itkDCMTKImageIO.cxx @@ -478,6 +478,7 @@ DCMTKImageIO::ReadImageInformation() this->m_Spacing.push_back(1.0); } + reader.PopulateMetaDataDictionary(this->GetMetaDataDictionary()); this->OpenDicomImage(); const DiPixel * interData = this->m_DImage->getInterData(); diff --git a/Modules/IO/DCMTK/test/CMakeLists.txt b/Modules/IO/DCMTK/test/CMakeLists.txt index 9251879a44ba..cd6f69dd0474 100644 --- a/Modules/IO/DCMTK/test/CMakeLists.txt +++ b/Modules/IO/DCMTK/test/CMakeLists.txt @@ -2,6 +2,7 @@ itk_module_test() set( ITKIODCMTKTests itkDCMTKGetDicomTagsTest.cxx + itkDCMTKImageIOMetadataTest.cxx itkDCMTKImageIOMultiFrameImageTest.cxx itkDCMTKImageIONoPreambleTest.cxx itkDCMTKImageIOOrthoDirTest.cxx @@ -82,6 +83,14 @@ itk_add_test( ${ITK_TEST_OUTPUT_DIR}/DICOMTags.txt ) +itk_add_test( + NAME itkDCMTKImageIOMetadataTest + COMMAND + ITKIODCMTKTestDriver + itkDCMTKImageIOMetadataTest + DATA{${ITK_DATA_ROOT}/Input/DicomSeries/Image0075.dcm} +) + itk_add_test( NAME itkDCMTKImageIOTest1 COMMAND diff --git a/Modules/IO/DCMTK/test/itkDCMTKImageIOMetadataTest.cxx b/Modules/IO/DCMTK/test/itkDCMTKImageIOMetadataTest.cxx new file mode 100644 index 000000000000..c9f33a104e20 --- /dev/null +++ b/Modules/IO/DCMTK/test/itkDCMTKImageIOMetadataTest.cxx @@ -0,0 +1,76 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ + +#include "itkDCMTKImageIO.h" +#include "itkMetaDataObject.h" +#include "itkTestingMacros.h" + +#include + +// Tests that DCMTKImageIO::ReadImageInformation() populates the metadata +// dictionary with DICOM tag values using "GGGG|EEEE" keys (lowercase +// matching GDCMImageIO behavior). Expected values are verified against +// Input/DicomSeries/Image0075.dcm using the same fixture as +// itkDCMTKGetDicomTagsTest. +int +itkDCMTKImageIOMetadataTest(int argc, char * argv[]) +{ + if (argc != 2) + { + std::cerr << "Missing parameters." << std::endl; + std::cerr << "Usage: " << itkNameOfTestExecutableMacro(argv) << " " << std::endl; + return EXIT_FAILURE; + } + + auto dcmtkIO = itk::DCMTKImageIO::New(); + + ITK_TEST_EXPECT_TRUE(dcmtkIO->CanReadFile(argv[1])); + dcmtkIO->SetFileName(argv[1]); + ITK_TRY_EXPECT_NO_EXCEPTION(dcmtkIO->ReadImageInformation()); + + const itk::MetaDataDictionary & dict = dcmtkIO->GetMetaDataDictionary(); + + // Dictionary must be populated + ITK_TEST_EXPECT_TRUE(!dict.GetKeys().empty()); + + // Pixel data (7FE0,0010) must NOT appear in the dictionary + ITK_TEST_EXPECT_TRUE(!dict.HasKey("7fe0|0010")); + + // Verify string-valued DICOM tags using the "GGGG|EEEE" key format. + // Values are specific to the test file Input/DicomSeries/Image0075.dcm + std::string value; + + // (0008,0021) DA SeriesDate + ITK_TEST_EXPECT_TRUE(itk::ExposeMetaData(dict, "0008|0021", value)); + ITK_TEST_EXPECT_EQUAL(value, std::string("20030625")); + + // (0010,0010) PN PatientName + ITK_TEST_EXPECT_TRUE(itk::ExposeMetaData(dict, "0010|0010", value)); + ITK_TEST_EXPECT_EQUAL(value, std::string("Wes Turner")); + + // (0010,0040) CS PatientSex + ITK_TEST_EXPECT_TRUE(itk::ExposeMetaData(dict, "0010|0040", value)); + ITK_TEST_EXPECT_EQUAL(value, std::string("O")); + + // (0028,0004) CS PhotometricInterpretation + ITK_TEST_EXPECT_TRUE(itk::ExposeMetaData(dict, "0028|0004", value)); + ITK_TEST_EXPECT_EQUAL(value, std::string("MONOCHROME2")); + + std::cout << "Test finished." << std::endl; + return EXIT_SUCCESS; +}