From 2467b157ea07dfb5f67bdca53687e2ac8846c1ac Mon Sep 17 00:00:00 2001 From: Hans Johnson Date: Fri, 1 May 2026 19:29:19 -0500 Subject: [PATCH 1/2] COMP: Expose DCMTK headers to ITKIODCMTKTestDriver (issue #3820) The ITK module ITKIODCMTK declares ITKDCMTK as PRIVATE_DEPENDS so the DCMTK headers do not leak through the toolkit's public include surface. But the public header itkDCMTKFileReader.h transitively #includes , so any test file that includes itkDCMTKFileReader.h fails to compile with: fatal error: 'dcmtk/dcmdata/dcdict.h' file not found This blocked PR #2796 from landing for over three years and is tracked as issue #3820. Add a narrowly-scoped target_include_directories() block in Modules/IO/DCMTK/test/CMakeLists.txt that adds the DCMTK ExternalProject's per-library include dirs and the generated configuration header dir to ITKIODCMTKTestDriver only. No change to the public DEPENDS surface of the IODCMTK module --- only the test driver target acquires the additional include paths. When ITK is built with ITK_USE_SYSTEM_DCMTK=ON the system-installed DCMTK headers are already on the default search path, so the block is gated behind NOT ITK_USE_SYSTEM_DCMTK. --- Modules/IO/DCMTK/test/CMakeLists.txt | 56 ++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/Modules/IO/DCMTK/test/CMakeLists.txt b/Modules/IO/DCMTK/test/CMakeLists.txt index 448887ef0af..a4e1ebe055c 100644 --- a/Modules/IO/DCMTK/test/CMakeLists.txt +++ b/Modules/IO/DCMTK/test/CMakeLists.txt @@ -16,6 +16,62 @@ set( createtestdriver(ITKIODCMTK "${ITKIODCMTK-Test_LIBRARIES}" "${ITKIODCMTKTests}") +# itkDCMTKFileReader.h transitively #include's dcmtk/dcmdata/*.h. ITKIODCMTK +# declares ITKDCMTK as a PRIVATE_DEPENDS (so consumers don't see DCMTK +# headers), but the test driver consumes the public itkDCMTKFileReader.h +# directly and therefore needs the DCMTK include path on its compile line. +# Tracks issue #3820. Reconstruct the include path the same way +# Modules/ThirdParty/DCMTK/CMakeLists.txt populates ITKDCMTK_INCLUDE_DIRS, +# since that variable is not in scope here. +if(NOT ITK_USE_SYSTEM_DCMTK) + set( + _dcmtk_ep_inc + "${CMAKE_BINARY_DIR}/Modules/ThirdParty/DCMTK/ITKDCMTK_ExtProject" + ) + set( + _dcmtk_libs + dcmdata + dcmpstat + dcmsr + dcmqrdb + dcmimgle + dcmimage + dcmjpeg + dcmjpls + dcmnet + dcmwlm + dcmrt + dcmiod + dcmfg + dcmseg + dcmpmap + ofstd + oflog + ) + # IS_DIRECTORY guards were dropped: the ExtProject source/build trees are + # only materialized at build time, not at CMake configure time, so any + # IS_DIRECTORY check here returns false on a clean checkout (issue #6189 + # greptile P1). The paths below are deterministic, and CMake is fine with + # -I flags that name not-yet-existing directories — they just need to + # exist by the time the test driver is compiled, which is guaranteed by + # ITKDCMTK_ExtProject being a transitive build dependency of the test + # driver via the ITKIODCMTK module's PRIVATE_DEPENDS. + foreach(_lib IN LISTS _dcmtk_libs) + target_include_directories( + ITKIODCMTKTestDriver + PRIVATE + "${_dcmtk_ep_inc}/${_lib}/include" + ) + endforeach() + target_include_directories( + ITKIODCMTKTestDriver + PRIVATE + "${CMAKE_BINARY_DIR}/Modules/ThirdParty/DCMTK/ITKDCMTK_ExtProject-build/config/include" + ) + unset(_dcmtk_libs) + unset(_dcmtk_ep_inc) +endif() + itk_add_test( NAME itkDCMTKImageIOTest1 COMMAND From 365b2baadc661f312db2a9e157ea8f236a951ab9 Mon Sep 17 00:00:00 2001 From: Hans Johnson Date: Fri, 1 May 2026 19:30:44 -0500 Subject: [PATCH 2/2] ENH: Add itkDCMTKGetDicomTagsTest covering DCMTKFileReader::GetElement* Add a test that exercises every well-formed VR-typed accessor on itk::DCMTKFileReader against the existing fixture Input/DicomSeries/Image0075.dcm: GetElementDA StudyDate GetElementTM StudyTime GetElementPN PatientName GetElementCS PatientSex, PhotometricInterpretation GetElementDS PatientWeight GetElementIS EchoNumber GetElementFL AcquisitionDuration (private GE tag) GetElementSL LocsPer3DSlab (private GE tag) GetElementUS SamplesPerPixel, Rows, BitsAllocated GetElementUI ScannerStudyEntityUID Expected values were verified out-of-band with pydicom against the on-disk fixture before authoring, fixing several wrong-by-transcription expectations from the original PR #2796 (e.g. AcquisitionDuration was recorded there as 1304791694, the integer reinterpretation of the float bit pattern; the actual scanner-reported FL value is 414273984.0). Each tag is asserted in-process via ITK_TEST_EXPECT_EQUAL; the test also writes one tag-value-per-line to argv[2] for diagnostic inspection. The in-process assertions are the source of truth for correctness, so no --compare baseline file is needed. The fixture is the single slice Image0075.dcm rather than the DicomSeries regex; the test reads exactly one file via SetFileName, not a series. Two further test invocations proposed by PR #2796 (against RGBDicomSeries/raw-RGB.dcm and RTDoseDicomSeries/32bitDoseRTImage.dcm) are dropped for now because those fixtures were never published to ITKTestingData. Supersedes PR #2796. Co-Authored-By: jhlegarreta --- Modules/IO/DCMTK/test/CMakeLists.txt | 10 ++ .../DCMTK/test/itkDCMTKGetDicomTagsTest.cxx | 165 ++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 Modules/IO/DCMTK/test/itkDCMTKGetDicomTagsTest.cxx diff --git a/Modules/IO/DCMTK/test/CMakeLists.txt b/Modules/IO/DCMTK/test/CMakeLists.txt index a4e1ebe055c..9251879a44b 100644 --- a/Modules/IO/DCMTK/test/CMakeLists.txt +++ b/Modules/IO/DCMTK/test/CMakeLists.txt @@ -1,6 +1,7 @@ itk_module_test() set( ITKIODCMTKTests + itkDCMTKGetDicomTagsTest.cxx itkDCMTKImageIOMultiFrameImageTest.cxx itkDCMTKImageIONoPreambleTest.cxx itkDCMTKImageIOOrthoDirTest.cxx @@ -72,6 +73,15 @@ if(NOT ITK_USE_SYSTEM_DCMTK) unset(_dcmtk_ep_inc) endif() +itk_add_test( + NAME itkDCMTKGetDicomTagsTest + COMMAND + ITKIODCMTKTestDriver + itkDCMTKGetDicomTagsTest + DATA{${ITK_DATA_ROOT}/Input/DicomSeries/Image0075.dcm} + ${ITK_TEST_OUTPUT_DIR}/DICOMTags.txt +) + itk_add_test( NAME itkDCMTKImageIOTest1 COMMAND diff --git a/Modules/IO/DCMTK/test/itkDCMTKGetDicomTagsTest.cxx b/Modules/IO/DCMTK/test/itkDCMTKGetDicomTagsTest.cxx new file mode 100644 index 00000000000..9b651c1f512 --- /dev/null +++ b/Modules/IO/DCMTK/test/itkDCMTKGetDicomTagsTest.cxx @@ -0,0 +1,165 @@ +/*========================================================================= + * + * 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 "itkDCMTKFileReader.h" +#include "itkIntTypes.h" +#include "itkTestingMacros.h" + +#include +#include + + +// Exercises every well-formed VR-typed accessor on itk::DCMTKFileReader against +// a known fixture (Input/DicomSeries/Image0075.dcm). Expected values were +// verified out-of-band with pydicom; see PR description for the discovery +// procedure. Correctness is asserted in-process via ITK_TEST_EXPECT_EQUAL; +// argv[2] receives one tag value per line for diagnostic inspection only. +int +itkDCMTKGetDicomTagsTest(int argc, char * argv[]) +{ + if (argc != 3) + { + std::cerr << "Missing parameters." << std::endl; + std::cerr << "Usage: " << itkNameOfTestExecutableMacro(argv) << " " << std::endl; + return EXIT_FAILURE; + } + + itk::DCMTKFileReader fileReader; + fileReader.SetFileName(argv[1]); + ITK_TRY_EXPECT_NO_EXCEPTION(fileReader.LoadFile()); + + std::ofstream outputFile(argv[2], std::ios::out); + if (!outputFile.is_open()) + { + std::cerr << "Unable to open output file: " << argv[2] << std::endl; + return EXIT_FAILURE; + } + + // Per-call: throwException=false so a missing tag returns EXIT_FAILURE + // and we surface a test failure instead of an exception. + constexpr bool throwException = false; + + // (0008,0021) DA StudyDate + { + std::string actual; + ITK_TEST_EXPECT_EQUAL(fileReader.GetElementDA(0x0008, 0x0021, actual, throwException), EXIT_SUCCESS); + outputFile << actual << std::endl; + ITK_TEST_EXPECT_EQUAL(actual, std::string("20030625")); + } + + // (0008,0030) TM StudyTime + { + std::string actual; + ITK_TEST_EXPECT_EQUAL(fileReader.GetElementTM(0x0008, 0x0030, actual, throwException), EXIT_SUCCESS); + outputFile << actual << std::endl; + ITK_TEST_EXPECT_EQUAL(actual, std::string("152734")); + } + + // (0010,0010) PN PatientName + { + std::string actual; + ITK_TEST_EXPECT_EQUAL(fileReader.GetElementPN(0x0010, 0x0010, actual, throwException), EXIT_SUCCESS); + outputFile << actual << std::endl; + ITK_TEST_EXPECT_EQUAL(actual, std::string("Wes Turner")); + } + + // (0010,0040) CS PatientSex + { + std::string actual; + ITK_TEST_EXPECT_EQUAL(fileReader.GetElementCS(0x0010, 0x0040, actual, throwException), EXIT_SUCCESS); + outputFile << actual << std::endl; + ITK_TEST_EXPECT_EQUAL(actual, std::string("O")); + } + + // (0010,1030) DS PatientWeight (single-element decimal-string) + { + std::string actual; + ITK_TEST_EXPECT_EQUAL(fileReader.GetElementDS(0x0010, 0x1030, actual, throwException), EXIT_SUCCESS); + outputFile << actual << std::endl; + ITK_TEST_EXPECT_EQUAL(actual, std::string("68.039")); + } + + // (0018,0086) IS EchoNumber + { + itk::int32_t actual = 0; + ITK_TEST_EXPECT_EQUAL(fileReader.GetElementIS(0x0018, 0x0086, actual, throwException), EXIT_SUCCESS); + outputFile << actual << std::endl; + ITK_TEST_EXPECT_EQUAL(actual, itk::int32_t{ 1 }); + } + + // (0019,105a) FL AcquisitionDuration (private tag, in GE private dictionary) + { + float actual = 0.0F; + ITK_TEST_EXPECT_EQUAL(fileReader.GetElementFL(0x0019, 0x105A, actual, throwException), EXIT_SUCCESS); + outputFile << actual << std::endl; + ITK_TEST_EXPECT_EQUAL(actual, 414273984.0F); + } + + // (0021,1057) SL LocsPer3DSlab (private tag) + { + itk::int32_t actual = 0; + ITK_TEST_EXPECT_EQUAL(fileReader.GetElementSL(0x0021, 0x1057, actual, throwException), EXIT_SUCCESS); + outputFile << actual << std::endl; + ITK_TEST_EXPECT_EQUAL(actual, itk::int32_t{ 124 }); + } + + // (0028,0002) US SamplesPerPixel + { + unsigned short actual = 0; + ITK_TEST_EXPECT_EQUAL(fileReader.GetElementUS(0x0028, 0x0002, actual, throwException), EXIT_SUCCESS); + outputFile << actual << std::endl; + ITK_TEST_EXPECT_EQUAL(actual, static_cast(1)); + } + + // (0028,0004) CS PhotometricInterpretation + { + std::string actual; + ITK_TEST_EXPECT_EQUAL(fileReader.GetElementCS(0x0028, 0x0004, actual, throwException), EXIT_SUCCESS); + outputFile << actual << std::endl; + ITK_TEST_EXPECT_EQUAL(actual, std::string("MONOCHROME2")); + } + + // (0028,0010) US Rows + { + unsigned short actual = 0; + ITK_TEST_EXPECT_EQUAL(fileReader.GetElementUS(0x0028, 0x0010, actual, throwException), EXIT_SUCCESS); + outputFile << actual << std::endl; + ITK_TEST_EXPECT_EQUAL(actual, static_cast(256)); + } + + // (0028,0100) US BitsAllocated + { + unsigned short actual = 0; + ITK_TEST_EXPECT_EQUAL(fileReader.GetElementUS(0x0028, 0x0100, actual, throwException), EXIT_SUCCESS); + outputFile << actual << std::endl; + ITK_TEST_EXPECT_EQUAL(actual, static_cast(16)); + } + + // (0043,1061) UI ScannerStudyEntityUID (GE private; in the DCMTK private + // dictionary so VR resolves to UI) + { + std::string actual; + ITK_TEST_EXPECT_EQUAL(fileReader.GetElementUI(0x0043, 0x1061, actual, throwException), EXIT_SUCCESS); + outputFile << actual << std::endl; + ITK_TEST_EXPECT_EQUAL(actual, std::string("1.2.840.113619.2.133.1762890640.1886.1055165015.961")); + } + + outputFile.close(); + std::cout << "Test finished." << std::endl; + return EXIT_SUCCESS; +}