diff --git a/CMakeLists.txt b/CMakeLists.txt index 1809f64..0c68d16 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,7 +30,7 @@ include(CMake/dcmtkPrepare.cmake NO_POLICY_SCOPE) # dcmimage dcmjpeg dcmjpls dcmtls dcmnet # CACHE STRING "List of modules that should be built.") -set(DCMTK_MODULES ofstd oflog dcmdata dcmnet +set(DCMTK_MODULES ofstd oflog dcmdata dcmnet dcmjpeg dcmjpls dcmimage dcmimgle CACHE STRING "List of modules that should be built.") #----------------------------------------------------------------------------- @@ -88,7 +88,7 @@ add_library(${PROJECT_NAME} SHARED # Define dependency libraries #---------------------------- -target_link_libraries(${PROJECT_NAME} dcmnet dcmdata oflog ofstd) +target_link_libraries(${PROJECT_NAME} ${DCMTK_MODULES}) # Gives our library file a .node extension without any "lib" prefix set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node") diff --git a/README.md b/README.md index 830c8de..dd00343 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,12 @@ Nodejs native addon for DICOM DIMSE services using the DCMTK DICOM c++ toolkit. * C-Find-scu * C-Move-scu * C-Get-scu +* C-Store-scu * C-Store-scp # Roadmap: -* C-Store-scu * extended characterSet support +* JPEG 2000 support (c-store scu) ## How to install This package uses prebuild to fetch precompiled binaries, so provided your platform is supported, all you need to do is: @@ -27,6 +28,9 @@ Otherwise install will try to compile the sources for your platform, you will ne ## Examples +run the examples: +```npm run example:[echo|find|get|move|store]``` + # Store-SCP ``` const dimse = require('dicom-dimse-native'); @@ -38,7 +42,7 @@ dimse.startScp(JSON.stringify( "ip" : "127.0.0.1", "port": "9999" }, - "storagePath": "./data" + "storagePath": "./data" // Directory where incoming DICOM files will be stored } ), (result) => { try { @@ -100,7 +104,7 @@ dimse.getScu(JSON.stringify( "ip" : "127.0.0.1", "port": "5678" }, - "storagePath": "./data" + "storagePath": "./data" // Directory where incoming DICOM files will be stored "tags" : [ { "key": "0020000D", @@ -122,6 +126,32 @@ dimse.getScu(JSON.stringify( }); ``` +# Store-SCU +``` +dimse.storeScu(JSON.stringify( + { + "source": { + "aet": "DCMTK", + "ip" : "127.0.0.1", + "port": "9999" + }, + "target": { + "aet": "CONQUESTSRV1", + "ip" : "127.0.0.1", + "port": "5678" + }, + "sourcePath": "./input" // Directory with DICOM files to be send + } +), (result) => { + try { + console.log(JSON.parse(result)); + } + catch { + console.log(result); + } +}); +``` + # Find-SCU ``` dimse.findScu(JSON.stringify( diff --git a/dcmimage/CMakeLists.txt b/dcmimage/CMakeLists.txt index 7a90faf..0d36da9 100644 --- a/dcmimage/CMakeLists.txt +++ b/dcmimage/CMakeLists.txt @@ -5,6 +5,6 @@ project(dcmimage) include_directories("${dcmimage_SOURCE_DIR}/include" "${ofstd_SOURCE_DIR}/include" "${oflog_SOURCE_DIR}/include" "${dcmdata_SOURCE_DIR}/include" "${dcmimgle_SOURCE_DIR}/include" ${ZLIB_INCDIR} ${LIBTIFF_INCDIR} ${LIBPNG_INCDIR}) # recurse into subdirectories -foreach(SUBDIR libsrc apps include) +foreach(SUBDIR libsrc include) add_subdirectory(${SUBDIR}) endforeach() diff --git a/dcmimgle/CMakeLists.txt b/dcmimgle/CMakeLists.txt index d47992c..78af4e7 100644 --- a/dcmimgle/CMakeLists.txt +++ b/dcmimgle/CMakeLists.txt @@ -5,6 +5,6 @@ project(dcmimgle) include_directories("${dcmimgle_SOURCE_DIR}/include" "${ofstd_SOURCE_DIR}/include" "${oflog_SOURCE_DIR}/include" "${dcmdata_SOURCE_DIR}/include" ${ZLIB_INCDIR}) # recurse into subdirectories -foreach(SUBDIR libsrc apps include data) +foreach(SUBDIR libsrc include data) add_subdirectory(${SUBDIR}) endforeach() diff --git a/dcmjpeg/CMakeLists.txt b/dcmjpeg/CMakeLists.txt index 0e995bf..a165aa8 100644 --- a/dcmjpeg/CMakeLists.txt +++ b/dcmjpeg/CMakeLists.txt @@ -2,6 +2,6 @@ project(dcmjpeg) # recurse into subdirectories -foreach(SUBDIR libsrc libijg8 libijg12 libijg16 apps include) +foreach(SUBDIR libsrc libijg8 libijg12 libijg16 include) add_subdirectory(${SUBDIR}) endforeach() diff --git a/dcmjpls/CMakeLists.txt b/dcmjpls/CMakeLists.txt index c8a05a7..027bfbb 100644 --- a/dcmjpls/CMakeLists.txt +++ b/dcmjpls/CMakeLists.txt @@ -2,6 +2,6 @@ project(dcmjpls) # recurse into subdirectories -foreach(SUBDIR libsrc libcharls apps include) +foreach(SUBDIR libsrc libcharls include) add_subdirectory(${SUBDIR}) endforeach() diff --git a/examples/store.js b/examples/store.js new file mode 100644 index 0000000..012e18f --- /dev/null +++ b/examples/store.js @@ -0,0 +1,28 @@ +const addon = require('../index'); + +addon.storeScu(JSON.stringify( + { + "source": { + "aet": "IMEBRA", + "ip" : "127.0.0.1", + "port": "9999" + }, + "target": { + "aet": "CONQUESTSRV1", + "ip" : "127.0.0.1", + "port": "5678" + }, + "sourcePath": "./examples/dicom", + "verbose": true + } +), (result) => { + if (result && result.length > 0) { + try + { + console.log(JSON.parse(result)); + } + catch (e) { + console.log(e, result); + } + } +}); diff --git a/package.json b/package.json index 5a88d9d..3d6835b 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,15 @@ { "name": "dicom-dimse-native", - "version": "1.0.9", + "version": "1.1.0", "description": "native addon using DCMTK dicom toolkit", "main": "index.js", "scripts": { "install": "npx prebuild-install -r napi || npx cmake-js compile", + "example:echo": "node ./examples/echo.js", + "example:find": "node ./examples/find.js", + "example:get": "node ./examples/get.js", + "example:move": "node ./examples/move.js", + "example:store": "node ./examples/store.js", "test": "mocha" }, "keywords": [ @@ -15,7 +20,9 @@ "DICOM-DIMSE-NATIVE", "DCMTK", "NETWORK", - "DICOMJS" + "DICOMJS", + "SCU", + "SCP" ], "author": "Michael Knopke", "license": "ISC", diff --git a/src/Addon.cc b/src/Addon.cc index 3c37116..a5cd609 100644 --- a/src/Addon.cc +++ b/src/Addon.cc @@ -4,6 +4,7 @@ #include "FindAsyncWorker.h" #include "GetAsyncWorker.h" #include "MoveAsyncWorker.h" +#include "StoreAsyncWorker.h" #include "ServerAsyncWorker.h" using namespace Napi; @@ -44,6 +45,15 @@ Value DoMove(const CallbackInfo& info) { return info.Env().Undefined(); } +Value DoStore(const CallbackInfo& info) { + std::string input = info[0].As().Utf8Value(); + Function cb = info[1].As(); + + auto worker = new StoreAsyncWorker(input, cb); + worker->Queue(); + return info.Env().Undefined(); +} + Value StartScp(const CallbackInfo& info) { std::string input = info[0].As().Utf8Value(); Function cb = info[1].As(); @@ -63,6 +73,8 @@ Object Init(Env env, Object exports) { Function::New(env, DoGet)); exports.Set(String::New(env, "moveScu"), Function::New(env, DoMove)); + exports.Set(String::New(env, "storeScu"), + Function::New(env, DoStore)); exports.Set(String::New(env, "startScp"), Function::New(env, StartScp)); return exports; diff --git a/src/GetAsyncWorker.cc b/src/GetAsyncWorker.cc index ed8353c..58e77cf 100644 --- a/src/GetAsyncWorker.cc +++ b/src/GetAsyncWorker.cc @@ -19,6 +19,7 @@ using json = nlohmann::json; #include "dcmtk/dcmdata/dcpath.h" /* for DcmPathProcessor */ #include "dcmtk/dcmdata/dcostrmz.h" /* for dcmZlibCompressionLevel */ + #ifdef WITH_ZLIB #include #endif @@ -128,6 +129,7 @@ namespace GetAsyncWorker::GetAsyncWorker(std::string data, Function &callback) : BaseAsyncWorker(data, callback) { + ns::registerCodecs(); } void GetAsyncWorker::Execute(const ExecutionProgress &progress) diff --git a/src/ServerAsyncWorker.cc b/src/ServerAsyncWorker.cc index c9b0705..78ae582 100644 --- a/src/ServerAsyncWorker.cc +++ b/src/ServerAsyncWorker.cc @@ -390,6 +390,7 @@ OFCondition acceptAssociation(T_ASC_Network *net, DcmAssociationConfiguration &a ServerAsyncWorker::ServerAsyncWorker(std::string data, Function &callback) : BaseAsyncWorker(data, callback) { + ns::registerCodecs(); } void ServerAsyncWorker::Execute(const ExecutionProgress &progress) diff --git a/src/StoreAsyncWorker.cc b/src/StoreAsyncWorker.cc new file mode 100644 index 0000000..fe6f195 --- /dev/null +++ b/src/StoreAsyncWorker.cc @@ -0,0 +1,664 @@ +#include "StoreAsyncWorker.h" + +#include +#include +#include +#include + +#include "json.h" +#include "Utils.h" + +using json = nlohmann::json; + +#include "dcmtk/config/osconfig.h" /* make sure OS specific configuration is included first */ +#include "dcmtk/ofstd/ofstdinc.h" +#include "dcmtk/ofstd/ofstd.h" +#include "dcmtk/ofstd/ofconapp.h" +#include "dcmtk/ofstd/ofstring.h" +#include "dcmtk/ofstd/ofstream.h" +#include "dcmtk/dcmnet/dicom.h" /* for DICOM_APPLICATION_REQUESTOR */ +#include "dcmtk/dcmnet/dimse.h" +#include "dcmtk/dcmnet/diutil.h" +#include "dcmtk/dcmnet/dcasccfg.h" /* for class DcmAssociationConfiguration */ +#include "dcmtk/dcmnet/dcasccff.h" /* for class DcmAssociationConfigurationFile */ +#include "dcmtk/dcmdata/dcdatset.h" +#include "dcmtk/dcmdata/dcmetinf.h" +#include "dcmtk/dcmdata/dcfilefo.h" +#include "dcmtk/dcmdata/dcuid.h" +#include "dcmtk/dcmdata/dcdict.h" +#include "dcmtk/dcmdata/dcdeftag.h" +#include "dcmtk/dcmdata/cmdlnarg.h" +#include "dcmtk/dcmdata/dcuid.h" /* for dcmtk version name */ +#include "dcmtk/dcmdata/dcostrmz.h" /* for dcmZlibCompressionLevel */ + + +#define INCLUDE_CSTDLIB +#define INCLUDE_CSTDIO +#define INCLUDE_CSTRING +#define INCLUDE_CSTDARG +#define INCLUDE_CERRNO +#define INCLUDE_CCTYPE +#define INCLUDE_CSIGNAL + +BEGIN_EXTERN_C +#ifdef HAVE_SYS_STAT_H +#include +#endif +#ifdef HAVE_FCNTL_H +#include /* needed on Solaris for O_RDONLY */ +#endif +#ifdef HAVE_SIGNAL_H +// On Solaris with Sun Workshop 11, declares signal() but does not +#include +#endif +END_EXTERN_C + +#ifdef HAVE_WINDOWS_H +#include /* for _mkdir() */ +#endif + +#if defined(HAVE_MKTEMP) && !defined(HAVE_PROTOTYPE_MKTEMP) +extern "C" { +//char * mktemp(char *); +} +#endif + +BEGIN_EXTERN_C +#ifdef HAVE_SYS_FILE_H +#include +#endif +END_EXTERN_C + + +#if defined (HAVE_WINDOWS_H) || defined(HAVE_FNMATCH_H) +#define PATTERN_MATCHING_AVAILABLE +#endif + +StoreAsyncWorker::StoreAsyncWorker(std::string data, Function &callback) : BaseAsyncWorker(data, callback) +{ + ns::registerCodecs(); + + m_networkTransferSyntax = EXS_Unknown; + m_readMode = ERM_autoDetect; + m_acse_timeout = 60; + m_dimse_timeout = 60; + m_proposeOnlyRequiredPresentationContexts = OFFalse; + m_lastCondition = EC_Normal; +} + +void StoreAsyncWorker::Execute(const ExecutionProgress &progress) +{ + ns::sInput in = ns::parseInputJson(_input); + + EnableVerboseLogging(in.verbose); + + if (!in.source.valid()) + { + SetErrorJson("Source not set"); + return; + } + + if (!in.target.valid()) + { + SetErrorJson("Target not set"); + return; + } + + if (!setScanDirectory(in.sourcePath.c_str())) { + SetErrorJson("Invalid source path set, no DICOM files found"); + return; + } + + bool success = sendStoreRequest(in.target.aet.c_str(), in.target.ip.c_str(), OFstatic_cast(Uint16, std::stoi(in.target.port)), + in.source.aet.c_str(), in.source.ip.c_str(), OFstatic_cast(Uint16, std::stoi(in.source.port)) ); + + if (!success) { + SetErrorJson("Failed to send DICOM files to target"); + } + +} + +bool StoreAsyncWorker::setScanDirectory(const OFFilename &dir) +{ + std::string fullPath(dir.getCharPointer()); + // unfortunately we need to convert slashes to backslashes on windows and vice versa on linux +#ifdef HAVE_WINDOWS_H + std::replace(fullPath.begin(),fullPath.end(),'/','\\'); +#else + std::replace(fullPath.begin(),fullPath.end(),'\\','/'); +#endif + + const OFFilename directory(fullPath.c_str(),true); + + // test if the directory exists: + if (!OFStandard::dirExists(directory)) { + DCMNET_ERROR("Directory does not exist" << directory); + return false; + } + + m_sourceDirectory = directory; + return true; + +} + +bool StoreAsyncWorker::sendStoreRequest(const OFString& peerTitle, const OFString& peerIP, Uint16 peerPort, + const OFString& ourTitle, const OFString& ourIP, Uint16 ourPort) +{ + bool result = true; + OFList inputFiles; + + OFList fileNameList; // list of files to transfer to SCP + OFList sopClassUIDList; // the list of SOP classes + OFList sopInstanceUIDList; // the list of SOP instances + + DIC_NODENAME peerHost; + DcmAssociationConfiguration asccfg; // handler for association configuration profiles + + OFString temp_str; + + T_ASC_Association* assoc = NULL; + T_ASC_Network* net = NULL; + T_ASC_Parameters* params = NULL; + + /* create list of input files */ + DCMNET_INFO("determining input files ..."); + + OFStandard::searchDirectoryRecursively(m_sourceDirectory, inputFiles, + OFFilename() /*Pattern */, OFFilename() /*dirPrefix*/, OFTrue); + + + /* check whether there are any input files at all */ + if (inputFiles.empty()) + { + DCMNET_ERROR("no input files to be sent"); + return false; + } + + /* check input files */ + OFString errormsg; + DcmFileFormat dfile; + char sopClassUID[128]; + char sopInstanceUID[128]; + OFBool ignoreName; + OFListIterator(OFFilename) if_iter = inputFiles.begin(); + OFListIterator(OFFilename) if_last = inputFiles.end(); + + DCMNET_INFO("checking input files ..."); + /* iterate over all input filenames */ + while (if_iter != if_last) + { + ignoreName = OFFalse; + const OFFilename & currentFilename = (*if_iter); + if (OFStandard::fileExists(currentFilename)) + { + if (!isDicomFile(currentFilename)) { + ignoreName = OFTrue; + } + + if (m_proposeOnlyRequiredPresentationContexts) + { + if (!findSOPClassAndInstanceInFile(currentFilename, sopClassUID, sopInstanceUID)) + { + ignoreName = OFTrue; + errormsg = "missing SOP class (or instance) in file: "; + errormsg += OFString(currentFilename.getCharPointer()); + DCMNET_WARN(errormsg << ", ignoring file"); + } + else if (!dcmIsaStorageSOPClassUID(sopClassUID)) + { + ignoreName = OFTrue; + errormsg = "unknown storage SOP class in file: "; + errormsg += OFString( currentFilename.getCharPointer() ); + errormsg += ": "; + errormsg += sopClassUID; + DCMNET_WARN(errormsg << ", ignoring file"); + } + else + { + sopClassUIDList.push_back(sopClassUID); + sopInstanceUIDList.push_back(sopInstanceUID); + } + } + if (!ignoreName) + { + fileNameList.push_back(currentFilename); + } + } + else + { + errormsg = "cannot access file: "; + errormsg += OFString(currentFilename.getCharPointer()); + DCMNET_WARN(errormsg << ", ignoring file"); + } + ++if_iter; + } + + /* do the real work, i.e. for all files which were specified in the */ + /* command line, transmit the encapsulated DICOM objects to the SCP. */ + OFListIterator(OFFilename) iter = fileNameList.begin(); + OFListIterator(OFFilename) enditer = fileNameList.end(); + + + /* initialize network, i.e. create an instance of T_ASC_Network*. */ + m_lastCondition = ASC_initializeNetwork(NET_REQUESTOR, 0, m_acse_timeout, &net); + if (m_lastCondition.bad()) { + DCMNET_ERROR(DimseCondition::dump(temp_str, m_lastCondition)); + return false; + } + + + /* initialize association parameters, i.e. create an instance of T_ASC_Parameters*. */ + m_lastCondition = ASC_createAssociationParameters(¶ms, ASC_DEFAULTMAXPDU); + if (m_lastCondition.bad()) { + DCMNET_ERROR(DimseCondition::dump(temp_str, m_lastCondition)); + return false; + } + /* sets this application's title and the called application's title in the params */ + /* structure. The default values to be set here are "STORESCU" and "ANY-SCP". */ + ASC_setAPTitles(params, ourTitle.c_str(), peerTitle.c_str(), ourTitle.c_str()); + + /* Set the transport layer type (type of network connection) in the params */ + /* structure. The default is an insecure connection; where OpenSSL is */ + /* available the user is able to request an encrypted,secure connection. */ + m_lastCondition = ASC_setTransportLayerType(params, OFFalse); + if (m_lastCondition.bad()) { + DCMNET_ERROR(DimseCondition::dump(temp_str, m_lastCondition)); + return false; + } + + /* Figure out the presentation addresses and copy the */ + /* corresponding values into the association parameters.*/ + sprintf(peerHost, "%s:%d", peerIP.c_str(), (int)peerPort); + ASC_setPresentationAddresses(params, OFStandard::getHostName().c_str(), peerHost); + + + /* Set the presentation contexts which will be negotiated */ + /* when the network connection will be established */ + m_lastCondition = addStoragePresentationContexts(params, sopClassUIDList); + + if (m_lastCondition.bad()) { + DCMNET_ERROR(DimseCondition::dump(temp_str, m_lastCondition)); + return false; + } + + DCMNET_INFO("Request Parameters:"); + DCMNET_INFO(ASC_dumpParameters(temp_str, params, ASC_ASSOC_RQ)); + + /* create association, i.e. try to establish a network connection to another */ + /* DICOM application. This call creates an instance of T_ASC_Association*. */ + DCMNET_INFO("Requesting Association"); + m_lastCondition = ASC_requestAssociation(net, params, &assoc, NULL, 0, DUL_BLOCK, m_dimse_timeout ); + if (m_lastCondition.bad()) { + if (m_lastCondition == DUL_ASSOCIATIONREJECTED) { + T_ASC_RejectParameters rej; + + ASC_getRejectParameters(params, &rej); + DCMNET_ERROR("Association Rejected:"); + DCMNET_ERROR(ASC_printRejectParameters(temp_str, &rej)); + } else { + DCMNET_ERROR("StoreScu - Association Request Failed: " << DimseCondition::dump(temp_str, m_lastCondition)); + } + return false; + } + + /* dump the connection parameters if in debug mode*/ + DCMNET_INFO(ASC_dumpConnectionParameters(temp_str, assoc)); + + + DCMNET_INFO("Association Parameters Negotiated:"); + DCMNET_INFO(ASC_dumpParameters(temp_str, params, ASC_ASSOC_AC)); + + /* count the presentation contexts which have been accepted by the SCP */ + /* If there are none, finish the execution */ + if (ASC_countAcceptedPresentationContexts(params) == 0) { + DCMNET_ERROR("No Acceptable Presentation Contexts"); + return false; + } + + /* dump general information concerning the establishment of the network connection if required */ + DCMNET_INFO("Association Accepted (Max Send PDV: " << assoc->sendPDVLength << ")"); + + float fileCount = static_cast(fileNameList.size()); + float count = 0; + while ((iter != enditer) && m_lastCondition.good()) + { + m_lastCondition = storeSCU(assoc, *iter); + ++iter; + ++count; + int progress = (int)(count / fileCount * 100.0f); + DCMNET_INFO("C-Store progress: " << progress); + } + + /* tear down association, i.e. terminate network connection to SCP */ + if (m_lastCondition == EC_Normal) + { + /* release association */ + DCMNET_INFO("Releasing Association"); + m_lastCondition = ASC_releaseAssociation(assoc); + if (m_lastCondition.bad()) + { + DCMNET_ERROR("Association Release Failed: " << DimseCondition::dump(temp_str, m_lastCondition)); + return false; + } + } + else if (m_lastCondition == DUL_PEERREQUESTEDRELEASE) + { + DCMNET_ERROR("Protocol Error: Peer requested release (Aborting)"); + DCMNET_INFO("Aborting Association"); + m_lastCondition = ASC_abortAssociation(assoc); + if (m_lastCondition.bad()) { + DCMNET_ERROR("Association Abort Failed: " << DimseCondition::dump(temp_str, m_lastCondition)); + return false; + } + } + else if (m_lastCondition == DUL_PEERABORTEDASSOCIATION) + { + DCMNET_INFO("Peer Aborted Association"); + } + else + { + DCMNET_ERROR(DimseCondition::dump(temp_str, m_lastCondition)); + DCMNET_INFO("Aborting Association"); + m_lastCondition = ASC_abortAssociation(assoc); + if (m_lastCondition.bad()) { + DCMNET_ERROR("Association Abort Failed: " << DimseCondition::dump(temp_str, m_lastCondition)); + return false; + } + } + + return result; +} + +OFBool StoreAsyncWorker::isaListMember(OFList &lst, OFString &s) +{ + OFListIterator(OFString) cur = lst.begin(); + OFListIterator(OFString) end = lst.end(); + + OFBool found = OFFalse; + while (cur != end && !found) { + found = (s == *cur); + ++cur; + } + + return found; +} + +OFCondition StoreAsyncWorker::addPresentationContext(T_ASC_Parameters *params, int presentationContextId, + const OFString &abstractSyntax, const OFString &transferSyntax, + T_ASC_SC_ROLE proposedRole) +{ + const char *c_p = transferSyntax.c_str(); + OFCondition cond = ASC_addPresentationContext(params, presentationContextId, + abstractSyntax.c_str(), &c_p, 1, proposedRole); + return cond; +} + +OFCondition StoreAsyncWorker::addPresentationContext(T_ASC_Parameters *params, int presentationContextId, + const OFString &abstractSyntax, const OFList &transferSyntaxList, + T_ASC_SC_ROLE proposedRole) +{ + // create an array of supported/possible transfer syntaxes + const char **transferSyntaxes = new const char*[transferSyntaxList.size()]; + int transferSyntaxCount = 0; + OFListConstIterator(OFString) s_cur = transferSyntaxList.begin(); + OFListConstIterator(OFString) s_end = transferSyntaxList.end(); + while (s_cur != s_end) { + transferSyntaxes[transferSyntaxCount++] = (*s_cur).c_str(); + ++s_cur; + } + + OFCondition cond = ASC_addPresentationContext(params, presentationContextId, + abstractSyntax.c_str(), transferSyntaxes, transferSyntaxCount, proposedRole); + + delete[] transferSyntaxes; + return cond; +} + +OFCondition StoreAsyncWorker::addStoragePresentationContexts(T_ASC_Parameters *params, + OFList &sopClasses) +{ + + // Which transfer syntax was preferred on the command line + OFString preferredTransferSyntax; + if (m_networkTransferSyntax == EXS_Unknown) { + /* gLocalByteOrder is defined in dcxfer.h */ + if (gLocalByteOrder == EBO_LittleEndian) { + /* we are on a little endian machine */ + preferredTransferSyntax = UID_LittleEndianExplicitTransferSyntax; + } else { + /* we are on a big endian machine */ + preferredTransferSyntax = UID_BigEndianExplicitTransferSyntax; + } + } else { + DcmXfer xfer(m_networkTransferSyntax); + preferredTransferSyntax = xfer.getXferID(); + } + + OFListIterator(OFString) s_cur; + OFListIterator(OFString) s_end; + + OFList fallbackSyntaxes; + // - If little endian implicit is preferred, we don't need any fallback syntaxes + // because it is the default transfer syntax and all applications must support it. + // - If MPEG2 MP@ML is preferred, we don't want to propose any fallback solution + // because this is not required and we cannot decompress the movie anyway. + if ((m_networkTransferSyntax != EXS_LittleEndianImplicit) && + (m_networkTransferSyntax != EXS_MPEG2MainProfileAtMainLevel)) + { + fallbackSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax); + fallbackSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax); + fallbackSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax); + // Remove the preferred syntax from the fallback list + fallbackSyntaxes.remove(preferredTransferSyntax); + } + + // create a list of transfer syntaxes combined from the preferred and fallback syntaxes + OFList combinedSyntaxes; + s_cur = fallbackSyntaxes.begin(); + s_end = fallbackSyntaxes.end(); + combinedSyntaxes.push_back(preferredTransferSyntax); + while (s_cur != s_end) + { + if (!isaListMember(combinedSyntaxes, *s_cur)) combinedSyntaxes.push_back(*s_cur); + ++s_cur; + } + + if (!m_proposeOnlyRequiredPresentationContexts) { + // add the (short list of) known storage SOP classes to the list + // the array of Storage SOP Class UIDs comes from dcuid.h + for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs; i++) + sopClasses.push_back(dcmShortSCUStorageSOPClassUIDs[i]); + } + + // thin out the SOP classes to remove any duplicates + OFList sops; + s_cur = sopClasses.begin(); + s_end = sopClasses.end(); + while (s_cur != s_end) { + if (!isaListMember(sops, *s_cur)) { + sops.push_back(*s_cur); + } + ++s_cur; + } + + // add a presentations context for each SOP class / transfer syntax pair + OFCondition cond = EC_Normal; + int pid = 1; // presentation context id + s_cur = sops.begin(); + s_end = sops.end(); + while (s_cur != s_end && cond.good()) { + + if (pid > 255) { + DCMNET_ERROR("Too many presentation contexts"); + return ASC_BADPRESENTATIONCONTEXTID; + } + + // SOP class with preferred transfer syntax + cond = addPresentationContext(params, pid, *s_cur, preferredTransferSyntax); + pid += 2; /* only odd presentation context id's */ + + if (fallbackSyntaxes.size() > 0) { + if (pid > 255) { + DCMNET_ERROR("Too many presentation contexts"); + return ASC_BADPRESENTATIONCONTEXTID; + } + + // SOP class with fallback transfer syntax + cond = addPresentationContext(params, pid, *s_cur, fallbackSyntaxes); + pid += 2; /* only odd presentation context id's */ + } + ++s_cur; + } + + return cond; +} + +void StoreAsyncWorker::progressCallback(void * /*callbackData*/, T_DIMSE_StoreProgress *progress, + T_DIMSE_C_StoreRQ * req) +{ + if (progress->state == DIMSE_StoreBegin) + { + OFString str; + DCMNET_INFO(DIMSE_dumpMessage(str, *req, DIMSE_OUTGOING)); + } + +} + +OFCondition StoreAsyncWorker::storeSCU(T_ASC_Association *assoc, const OFFilename &fname) +{ + DIC_US msgId = assoc->nextMsgID++; + T_ASC_PresentationContextID presID; + T_DIMSE_C_StoreRQ req; + T_DIMSE_C_StoreRSP rsp; + DIC_UI sopClass; + DIC_UI sopInstance; + DcmDataset *statusDetail = NULL; + + DCMNET_INFO("Sending file: " << fname); + + /* read information from file. After the call to DcmFileFormat::loadFile(...) the information */ + /* which is encapsulated in the file will be available through the DcmFileFormat object. */ + /* In detail, it will be available through calls to DcmFileFormat::getMetaInfo() (for */ + /* meta header information) and DcmFileFormat::getDataset() (for data set information). */ + DcmFileFormat dcmff; + OFCondition cond = dcmff.loadFile(fname, EXS_Unknown, EGL_noChange, DCM_MaxReadLength, m_readMode); + + /* figure out if an error occurred while the file was read*/ + if (cond.bad()) { + DCMNET_ERROR("Bad DICOM file: " << fname << ": " << cond.text()); + return cond; + } + + /* figure out which SOP class and SOP instance is encapsulated in the file */ + if (!DU_findSOPClassAndInstanceInDataSet(dcmff.getDataset(), sopClass, sizeof(sopClass), sopInstance, sizeof(sopInstance), OFFalse)) { + DCMNET_ERROR("No SOP Class or Instance UID in file: " << fname); + return DIMSE_BADDATA; + } + + /* figure out which of the accepted presentation contexts should be used */ + DcmXfer filexfer(dcmff.getDataset()->getOriginalXfer()); + + /* special case: if the file uses an unencapsulated transfer syntax (uncompressed + * or deflated explicit VR) and we prefer deflated explicit VR, then try + * to find a presentation context for deflated explicit VR first. + */ + if (filexfer.isNotEncapsulated() && + m_networkTransferSyntax == EXS_DeflatedLittleEndianExplicit) + { + filexfer = EXS_DeflatedLittleEndianExplicit; + } + + if (filexfer.getXfer() != EXS_Unknown) { + presID = ASC_findAcceptedPresentationContextID(assoc, sopClass, filexfer.getXferID()); + } else { + presID = ASC_findAcceptedPresentationContextID(assoc, sopClass); + } + + if (presID == 0) { + const char *modalityName = dcmSOPClassUIDToModality(sopClass); + if (!modalityName) modalityName = dcmFindNameOfUID(sopClass); + if (!modalityName) modalityName = "unknown SOP class"; + DCMNET_ERROR("No presentation context for: (" << modalityName << ") " << sopClass); + return DIMSE_NOVALIDPRESENTATIONCONTEXTID; + } + + T_ASC_PresentationContext pc; + ASC_findAcceptedPresentationContext(assoc->params, presID, &pc); + DcmXfer netTransfer(pc.acceptedTransferSyntax); + + /* if required, dump general information concerning transfer syntaxes */ + DcmXfer fileTransfer(dcmff.getDataset()->getOriginalXfer()); + DCMNET_INFO("Transfer Syntax: " << dcmFindNameOfUID(fileTransfer.getXferID()) + << " -> " << dcmFindNameOfUID(netTransfer.getXferID())); + + /* prepare the transmission of data */ + bzero((char *)&req, sizeof(req)); + req.MessageID = msgId; + strcpy(req.AffectedSOPClassUID, sopClass); + strcpy(req.AffectedSOPInstanceUID, sopInstance); + req.DataSetType = DIMSE_DATASET_PRESENT; + req.Priority = DIMSE_PRIORITY_LOW; + + /* if required, dump some more general information */ + DCMNET_INFO("Sending Store Request: MsgID " << msgId << ", (" + << dcmSOPClassUIDToModality(sopClass, "OT") << ")"); + + // need to recode depending on accepted xfer + DcmDataset* dataset = dcmff.getDataset(); + dataset->chooseRepresentation(netTransfer.getXfer(), NULL); + + // test + if (!dataset->canWriteXfer(netTransfer.getXfer())) { + DCMNET_ERROR("unable to write Xfer: " << netTransfer.getXferName()); + } + + /* finally conduct transmission of data */ + cond = DIMSE_storeUser(assoc, presID, &req, NULL, dataset, progressCallback, NULL, + DIMSE_BLOCKING, m_dimse_timeout, &rsp, &statusDetail, NULL, (long)OFStandard::getFileSize(fname)); + + /* dump some more general information */ + if (cond == EC_Normal) + { + OFString str; + DCMNET_INFO("Received Store Response"); + DCMNET_INFO(DIMSE_dumpMessage(str, rsp, DIMSE_INCOMING, NULL, presID)); + } + else + { + OFString temp_str; + DCMNET_ERROR("Store Failed, file: " << fname << ":"); + DCMNET_ERROR( DimseCondition::dump(temp_str, cond)); + } + + /* dump status detail information if there is some */ + if (statusDetail != NULL) { + DCMNET_INFO("Status Detail:"); + DCMNET_INFO(DcmObject::PrintHelper(*statusDetail)); + delete statusDetail; + } + /* return */ + return cond; +} + +OFBool StoreAsyncWorker::findSOPClassAndInstanceInFile(const OFFilename &fname, char *sopClass, + char *sopInstance) +{ + DcmFileFormat ff; + if (!ff.loadFile(fname, EXS_Unknown, EGL_noChange, DCM_MaxReadLength, m_readMode).good()) + return OFFalse; + + /* look in the meta-header first */ + OFBool found = DU_findSOPClassAndInstanceInDataSet(ff.getMetaInfo(), sopClass, sizeof(sopClass), sopInstance, sizeof(sopInstance), OFFalse); + + if (!found) + found = DU_findSOPClassAndInstanceInDataSet(ff.getDataset(), sopClass, sizeof(sopClass), sopInstance, sizeof(sopInstance), OFFalse); + + return found; +} + +OFBool StoreAsyncWorker::isDicomFile( const OFFilename &fname ) +{ + return OFTrue; +} + + diff --git a/src/StoreAsyncWorker.h b/src/StoreAsyncWorker.h new file mode 100644 index 0000000..8ec111c --- /dev/null +++ b/src/StoreAsyncWorker.h @@ -0,0 +1,67 @@ +#pragma once + +#include "BaseAsyncWorker.h" + + +#include "dcmtk/config/osconfig.h" /* make sure OS specific configuration is included first */ +#include "dcmtk/ofstd/ofcond.h" +#include "dcmtk/ofstd/ofstring.h" +#include "dcmtk/dcmnet/assoc.h" +#include "dcmtk/dcmnet/dimse.h" +#include "dcmtk/ofstd/ofstdinc.h" +#include "dcmtk/ofstd/offile.h" + + +using namespace Napi; + +class DcmDataset; +class DcmItem; + + +class StoreAsyncWorker : public BaseAsyncWorker +{ + public: + StoreAsyncWorker(std::string data, Function &callback); + + void Execute(const ExecutionProgress& progress); + + protected: + bool setScanDirectory(const OFFilename &dir); + + bool sendStoreRequest(const OFString& peerTitle, const OFString& peerIP, Uint16 peerPort, + const OFString& ourTitle, const OFString& ourIP, Uint16 ourPort); + + OFCondition storeSCU(T_ASC_Association *assoc, const OFFilename &fname); + + OFBool findSOPClassAndInstanceInFile(const OFFilename &fname, char *sopClass, char *sopInstance); + + OFBool isaListMember(OFList &lst, OFString &s); + + OFCondition addPresentationContext(T_ASC_Parameters *params, int presentationContextId, + const OFString &abstractSyntax, const OFString &transferSyntax, + T_ASC_SC_ROLE proposedRole = ASC_SC_ROLE_DEFAULT); + + OFCondition addPresentationContext(T_ASC_Parameters *params, int presentationContextId, + const OFString &abstractSyntax, const OFList &transferSyntaxList, + T_ASC_SC_ROLE proposedRole = ASC_SC_ROLE_DEFAULT); + + OFCondition addStoragePresentationContexts(T_ASC_Parameters *params, OFList &sopClasses); + + + static void progressCallback(void * /*callbackData*/, T_DIMSE_StoreProgress *progress, + T_DIMSE_C_StoreRQ * req); + + + OFBool isDicomFile(const OFFilename &fname); + +private: + + OFFilename m_sourceDirectory; + E_FileReadMode m_readMode; + OFBool m_proposeOnlyRequiredPresentationContexts; + OFCondition m_lastCondition; + E_TransferSyntax m_networkTransferSyntax; + unsigned long m_acse_timeout; + unsigned long m_dimse_timeout; + +}; diff --git a/src/Utils.h b/src/Utils.h index c3de270..e6487ba 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -17,6 +17,13 @@ using json = nlohmann::json; #include "dcmtk/dcmdata/dcxfer.h" /* for E_TransferSyntax */ #include "dcmtk/dcmnet/dimse.h" /* for T_DIMSE_BlockingMode */ +#include "dcmtk/dcmjpeg/djdecode.h" /* for dcmjpeg decoders */ +#include "dcmtk/dcmjpeg/djencode.h" /* for dcmjpeg encoders */ +#include "dcmtk/dcmdata/dcrledrg.h" /* for DcmRLEDecoderRegistration */ +#include "dcmtk/dcmdata/dcrleerg.h" /* for DcmRLEEncoderRegistration */ +#include "dcmtk/dcmjpls/djdecode.h" /* for dcmjpls decoder */ +#include "dcmtk/dcmjpls/djencode.h" /* for dcmjpls encoder */ + namespace ns { @@ -66,6 +73,7 @@ namespace ns { sIdent source; sIdent target; std::string storagePath; + std::string sourcePath; std::string destination; std::vector tags; bool verbose; @@ -84,6 +92,21 @@ namespace ns { return ""; } + static bool codecsRegistered = false; + + static void registerCodecs() { + if (!codecsRegistered) { + DcmRLEDecoderRegistration::registerCodecs(); + DJDecoderRegistration::registerCodecs(); + DJLSDecoderRegistration::registerCodecs(); + + DJEncoderRegistration::registerCodecs(); + DJLSEncoderRegistration::registerCodecs(); + DcmRLEEncoderRegistration::registerCodecs(); + codecsRegistered = true; + } + } + inline void to_json(json& j, const sTag& p) { j = json{{"key", p.key}, {"value", p.value}}; } @@ -115,6 +138,7 @@ namespace ns { } catch(...) {} in.destination = toString(j, "destination"); in.storagePath = toString(j, "storagePath"); + in.sourcePath = toString(j, "sourcePath"); try { auto tags = j.at("tags"); for (json::iterator it = tags.begin(); it != tags.end(); ++it) {