comparison UnitTestsSources/FromDcmtkTests.cpp @ 3827:638906dcfe32 transcoding

integration mainline->transcoding
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 10 Apr 2020 16:18:17 +0200
parents 6762506ef4fb
children 5bba4d249422
comparison
equal deleted inserted replaced
3824:6762506ef4fb 3827:638906dcfe32
2404 } 2404 }
2405 } 2405 }
2406 2406
2407 2407
2408 2408
2409 #ifdef _WIN32 2409 #include "../Core/DicomNetworking/DicomAssociation.h"
2410 /** 2410 #include "../Core/DicomNetworking/DicomControlUserConnection.h"
2411 * "The maximum length, in bytes, of the string returned in the buffer 2411 #include "../Core/DicomNetworking/DicomStoreUserConnection.h"
2412 * pointed to by the name parameter is dependent on the namespace provider,
2413 * but this string must be 256 bytes or less.
2414 * http://msdn.microsoft.com/en-us/library/windows/desktop/ms738527(v=vs.85).aspx
2415 **/
2416 # define HOST_NAME_MAX 256
2417 # include <winsock.h>
2418 #endif
2419
2420
2421 #if !defined(HOST_NAME_MAX) && defined(_POSIX_HOST_NAME_MAX)
2422 /**
2423 * TO IMPROVE: "_POSIX_HOST_NAME_MAX is only the minimum value that
2424 * HOST_NAME_MAX can ever have [...] Therefore you cannot allocate an
2425 * array of size _POSIX_HOST_NAME_MAX, invoke gethostname() and expect
2426 * that the result will fit."
2427 * http://lists.gnu.org/archive/html/bug-gnulib/2009-08/msg00128.html
2428 **/
2429 #define HOST_NAME_MAX _POSIX_HOST_NAME_MAX
2430 #endif
2431
2432
2433 #include "../Core/DicomNetworking/RemoteModalityParameters.h"
2434
2435
2436 #include <dcmtk/dcmnet/diutil.h> // For dcmConnectionTimeout()
2437
2438
2439
2440 namespace Orthanc
2441 {
2442 // By default, the timeout for client DICOM connections is set to 10 seconds
2443 static boost::mutex defaultTimeoutMutex_;
2444 static uint32_t defaultTimeout_ = 10;
2445
2446
2447 class DicomAssociationParameters
2448 {
2449 private:
2450 std::string localAet_;
2451 std::string remoteAet_;
2452 std::string remoteHost_;
2453 uint16_t remotePort_;
2454 ModalityManufacturer manufacturer_;
2455 uint32_t timeout_;
2456
2457 void ReadDefaultTimeout()
2458 {
2459 boost::mutex::scoped_lock lock(defaultTimeoutMutex_);
2460 timeout_ = defaultTimeout_;
2461 }
2462
2463 public:
2464 DicomAssociationParameters() :
2465 localAet_("STORESCU"),
2466 remoteAet_("ANY-SCP"),
2467 remoteHost_("127.0.0.1"),
2468 remotePort_(104),
2469 manufacturer_(ModalityManufacturer_Generic)
2470 {
2471 ReadDefaultTimeout();
2472 }
2473
2474 DicomAssociationParameters(const std::string& localAet,
2475 const RemoteModalityParameters& remote) :
2476 localAet_(localAet),
2477 remoteAet_(remote.GetApplicationEntityTitle()),
2478 remoteHost_(remote.GetHost()),
2479 remotePort_(remote.GetPortNumber()),
2480 manufacturer_(remote.GetManufacturer()),
2481 timeout_(defaultTimeout_)
2482 {
2483 ReadDefaultTimeout();
2484 }
2485
2486 const std::string& GetLocalApplicationEntityTitle() const
2487 {
2488 return localAet_;
2489 }
2490
2491 const std::string& GetRemoteApplicationEntityTitle() const
2492 {
2493 return remoteAet_;
2494 }
2495
2496 const std::string& GetRemoteHost() const
2497 {
2498 return remoteHost_;
2499 }
2500
2501 uint16_t GetRemotePort() const
2502 {
2503 return remotePort_;
2504 }
2505
2506 ModalityManufacturer GetRemoteManufacturer() const
2507 {
2508 return manufacturer_;
2509 }
2510
2511 void SetLocalApplicationEntityTitle(const std::string& aet)
2512 {
2513 localAet_ = aet;
2514 }
2515
2516 void SetRemoteApplicationEntityTitle(const std::string& aet)
2517 {
2518 remoteAet_ = aet;
2519 }
2520
2521 void SetRemoteHost(const std::string& host)
2522 {
2523 if (host.size() > HOST_NAME_MAX - 10)
2524 {
2525 throw OrthancException(ErrorCode_ParameterOutOfRange,
2526 "Invalid host name (too long): " + host);
2527 }
2528
2529 remoteHost_ = host;
2530 }
2531
2532 void SetRemotePort(uint16_t port)
2533 {
2534 remotePort_ = port;
2535 }
2536
2537 void SetRemoteManufacturer(ModalityManufacturer manufacturer)
2538 {
2539 manufacturer_ = manufacturer;
2540 }
2541
2542 void SetRemoteModality(const RemoteModalityParameters& parameters)
2543 {
2544 SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle());
2545 SetRemoteHost(parameters.GetHost());
2546 SetRemotePort(parameters.GetPortNumber());
2547 SetRemoteManufacturer(parameters.GetManufacturer());
2548 }
2549
2550 bool IsEqual(const DicomAssociationParameters& other) const
2551 {
2552 return (localAet_ == other.localAet_ &&
2553 remoteAet_ == other.remoteAet_ &&
2554 remoteHost_ == other.remoteHost_ &&
2555 remotePort_ == other.remotePort_ &&
2556 manufacturer_ == other.manufacturer_);
2557 }
2558
2559 void SetTimeout(uint32_t seconds)
2560 {
2561 timeout_ = seconds;
2562 }
2563
2564 uint32_t GetTimeout() const
2565 {
2566 return timeout_;
2567 }
2568
2569 bool HasTimeout() const
2570 {
2571 return timeout_ != 0;
2572 }
2573
2574 static void SetDefaultTimeout(uint32_t seconds)
2575 {
2576 LOG(INFO) << "Default timeout for DICOM connections if Orthanc acts as SCU (client): "
2577 << seconds << " seconds (0 = no timeout)";
2578
2579 {
2580 boost::mutex::scoped_lock lock(defaultTimeoutMutex_);
2581 defaultTimeout_ = seconds;
2582 }
2583 }
2584 };
2585
2586
2587 static void FillSopSequence(DcmDataset& dataset,
2588 const DcmTagKey& tag,
2589 const std::vector<std::string>& sopClassUids,
2590 const std::vector<std::string>& sopInstanceUids,
2591 const std::vector<StorageCommitmentFailureReason>& failureReasons,
2592 bool hasFailureReasons)
2593 {
2594 assert(sopClassUids.size() == sopInstanceUids.size() &&
2595 (hasFailureReasons ?
2596 failureReasons.size() == sopClassUids.size() :
2597 failureReasons.empty()));
2598
2599 if (sopInstanceUids.empty())
2600 {
2601 // Add an empty sequence
2602 if (!dataset.insertEmptyElement(tag).good())
2603 {
2604 throw OrthancException(ErrorCode_InternalError);
2605 }
2606 }
2607 else
2608 {
2609 for (size_t i = 0; i < sopClassUids.size(); i++)
2610 {
2611 std::unique_ptr<DcmItem> item(new DcmItem);
2612 if (!item->putAndInsertString(DCM_ReferencedSOPClassUID, sopClassUids[i].c_str()).good() ||
2613 !item->putAndInsertString(DCM_ReferencedSOPInstanceUID, sopInstanceUids[i].c_str()).good() ||
2614 (hasFailureReasons &&
2615 !item->putAndInsertUint16(DCM_FailureReason, failureReasons[i]).good()) ||
2616 !dataset.insertSequenceItem(tag, item.release()).good())
2617 {
2618 throw OrthancException(ErrorCode_InternalError);
2619 }
2620 }
2621 }
2622 }
2623
2624
2625 class DicomAssociation : public boost::noncopyable
2626 {
2627 private:
2628 // This is the maximum number of presentation context IDs (the
2629 // number of odd integers between 1 and 255)
2630 // http://dicom.nema.org/medical/dicom/2019e/output/chtml/part08/sect_9.3.2.2.html
2631 static const size_t MAX_PROPOSED_PRESENTATIONS = 128;
2632
2633 struct ProposedPresentationContext
2634 {
2635 std::string abstractSyntax_;
2636 std::set<DicomTransferSyntax> transferSyntaxes_;
2637 };
2638
2639 typedef std::map<std::string, std::map<DicomTransferSyntax, uint8_t> > AcceptedPresentationContexts;
2640
2641 DicomAssociationRole role_;
2642 bool isOpen_;
2643 std::vector<ProposedPresentationContext> proposed_;
2644 AcceptedPresentationContexts accepted_;
2645 T_ASC_Network* net_;
2646 T_ASC_Parameters* params_;
2647 T_ASC_Association* assoc_;
2648
2649 void Initialize()
2650 {
2651 role_ = DicomAssociationRole_Default;
2652 isOpen_ = false;
2653 net_ = NULL;
2654 params_ = NULL;
2655 assoc_ = NULL;
2656
2657 // Must be after "isOpen_ = false"
2658 ClearPresentationContexts();
2659 }
2660
2661 void CheckConnecting(const DicomAssociationParameters& parameters,
2662 const OFCondition& cond)
2663 {
2664 try
2665 {
2666 CheckCondition(cond, parameters, "connecting");
2667 }
2668 catch (OrthancException&)
2669 {
2670 CloseInternal();
2671 throw;
2672 }
2673 }
2674
2675 void CloseInternal()
2676 {
2677 if (assoc_ != NULL)
2678 {
2679 ASC_releaseAssociation(assoc_);
2680 ASC_destroyAssociation(&assoc_);
2681 assoc_ = NULL;
2682 params_ = NULL;
2683 }
2684 else
2685 {
2686 if (params_ != NULL)
2687 {
2688 ASC_destroyAssociationParameters(&params_);
2689 params_ = NULL;
2690 }
2691 }
2692
2693 if (net_ != NULL)
2694 {
2695 ASC_dropNetwork(&net_);
2696 net_ = NULL;
2697 }
2698
2699 accepted_.clear();
2700 isOpen_ = false;
2701 }
2702
2703 void AddAccepted(const std::string& abstractSyntax,
2704 DicomTransferSyntax syntax,
2705 uint8_t presentationContextId)
2706 {
2707 AcceptedPresentationContexts::iterator found = accepted_.find(abstractSyntax);
2708
2709 if (found == accepted_.end())
2710 {
2711 std::map<DicomTransferSyntax, uint8_t> syntaxes;
2712 syntaxes[syntax] = presentationContextId;
2713 accepted_[abstractSyntax] = syntaxes;
2714 }
2715 else
2716 {
2717 if (found->second.find(syntax) != found->second.end())
2718 {
2719 LOG(WARNING) << "The same transfer syntax ("
2720 << GetTransferSyntaxUid(syntax)
2721 << ") was accepted twice for the same abstract syntax UID ("
2722 << abstractSyntax << ")";
2723 }
2724 else
2725 {
2726 found->second[syntax] = presentationContextId;
2727 }
2728 }
2729 }
2730
2731 public:
2732 DicomAssociation()
2733 {
2734 Initialize();
2735 }
2736
2737 ~DicomAssociation()
2738 {
2739 try
2740 {
2741 Close();
2742 }
2743 catch (OrthancException&)
2744 {
2745 // Don't throw exception in destructors
2746 }
2747 }
2748
2749 bool IsOpen() const
2750 {
2751 return isOpen_;
2752 }
2753
2754 void SetRole(DicomAssociationRole role)
2755 {
2756 if (role_ != role)
2757 {
2758 Close();
2759 role_ = role;
2760 }
2761 }
2762
2763 void ClearPresentationContexts()
2764 {
2765 Close();
2766 proposed_.clear();
2767 proposed_.reserve(MAX_PROPOSED_PRESENTATIONS);
2768 }
2769
2770 void Open(const DicomAssociationParameters& parameters)
2771 {
2772 if (isOpen_)
2773 {
2774 return; // Already open
2775 }
2776
2777 // Timeout used during association negociation and ASC_releaseAssociation()
2778 uint32_t acseTimeout = parameters.GetTimeout();
2779 if (acseTimeout == 0)
2780 {
2781 /**
2782 * Timeout is disabled. Global timeout (seconds) for
2783 * connecting to remote hosts. Default value is -1 which
2784 * selects infinite timeout, i.e. blocking connect().
2785 **/
2786 dcmConnectionTimeout.set(-1);
2787 acseTimeout = 10;
2788 }
2789 else
2790 {
2791 dcmConnectionTimeout.set(acseTimeout);
2792 }
2793
2794 T_ASC_SC_ROLE dcmtkRole;
2795 switch (role_)
2796 {
2797 case DicomAssociationRole_Default:
2798 dcmtkRole = ASC_SC_ROLE_DEFAULT;
2799 break;
2800
2801 case DicomAssociationRole_Scu:
2802 dcmtkRole = ASC_SC_ROLE_SCU;
2803 break;
2804
2805 case DicomAssociationRole_Scp:
2806 dcmtkRole = ASC_SC_ROLE_SCP;
2807 break;
2808
2809 default:
2810 throw OrthancException(ErrorCode_ParameterOutOfRange);
2811 }
2812
2813 assert(net_ == NULL &&
2814 params_ == NULL &&
2815 assoc_ == NULL);
2816
2817 if (proposed_.empty())
2818 {
2819 throw OrthancException(ErrorCode_BadSequenceOfCalls,
2820 "No presentation context was proposed");
2821 }
2822
2823 LOG(INFO) << "Opening a DICOM SCU connection from AET \""
2824 << parameters.GetLocalApplicationEntityTitle()
2825 << "\" to AET \"" << parameters.GetRemoteApplicationEntityTitle()
2826 << "\" on host " << parameters.GetRemoteHost()
2827 << ":" << parameters.GetRemotePort()
2828 << " (manufacturer: " << EnumerationToString(parameters.GetRemoteManufacturer()) << ")";
2829
2830 CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_));
2831 CheckConnecting(parameters, ASC_createAssociationParameters(&params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU));
2832
2833 // Set this application's title and the called application's title in the params
2834 CheckConnecting(parameters, ASC_setAPTitles(
2835 params_, parameters.GetLocalApplicationEntityTitle().c_str(),
2836 parameters.GetRemoteApplicationEntityTitle().c_str(), NULL));
2837
2838 // Set the network addresses of the local and remote entities
2839 char localHost[HOST_NAME_MAX];
2840 gethostname(localHost, HOST_NAME_MAX - 1);
2841
2842 char remoteHostAndPort[HOST_NAME_MAX];
2843
2844 #ifdef _MSC_VER
2845 _snprintf
2846 #else
2847 snprintf
2848 #endif
2849 (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d",
2850 parameters.GetRemoteHost().c_str(), parameters.GetRemotePort());
2851
2852 CheckConnecting(parameters, ASC_setPresentationAddresses(params_, localHost, remoteHostAndPort));
2853
2854 // Set various options
2855 CheckConnecting(parameters, ASC_setTransportLayerType(params_, /*opt_secureConnection*/ false));
2856
2857 // Setup the list of proposed presentation contexts
2858 unsigned int presentationContextId = 1;
2859 for (size_t i = 0; i < proposed_.size(); i++)
2860 {
2861 assert(presentationContextId <= 255);
2862 const char* abstractSyntax = proposed_[i].abstractSyntax_.c_str();
2863
2864 const std::set<DicomTransferSyntax>& source = proposed_[i].transferSyntaxes_;
2865
2866 std::vector<const char*> transferSyntaxes;
2867 transferSyntaxes.reserve(source.size());
2868
2869 for (std::set<DicomTransferSyntax>::const_iterator
2870 it = source.begin(); it != source.end(); ++it)
2871 {
2872 transferSyntaxes.push_back(GetTransferSyntaxUid(*it));
2873 }
2874
2875 assert(!transferSyntaxes.empty());
2876 CheckConnecting(parameters, ASC_addPresentationContext(
2877 params_, presentationContextId, abstractSyntax,
2878 &transferSyntaxes[0], transferSyntaxes.size(), dcmtkRole));
2879
2880 presentationContextId += 2;
2881 }
2882
2883 // Do the association
2884 CheckConnecting(parameters, ASC_requestAssociation(net_, params_, &assoc_));
2885 isOpen_ = true;
2886
2887 // Inspect the accepted transfer syntaxes
2888 LST_HEAD **l = &params_->DULparams.acceptedPresentationContext;
2889 if (*l != NULL)
2890 {
2891 DUL_PRESENTATIONCONTEXT* pc = (DUL_PRESENTATIONCONTEXT*) LST_Head(l);
2892 LST_Position(l, (LST_NODE*)pc);
2893 while (pc)
2894 {
2895 if (pc->result == ASC_P_ACCEPTANCE)
2896 {
2897 DicomTransferSyntax transferSyntax;
2898 if (LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax))
2899 {
2900 AddAccepted(pc->abstractSyntax, transferSyntax, pc->presentationContextID);
2901 }
2902 else
2903 {
2904 LOG(WARNING) << "Unknown transfer syntax received from AET \""
2905 << parameters.GetRemoteApplicationEntityTitle()
2906 << "\": " << pc->acceptedTransferSyntax;
2907 }
2908 }
2909
2910 pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l);
2911 }
2912 }
2913
2914 if (accepted_.empty())
2915 {
2916 throw OrthancException(ErrorCode_NoPresentationContext,
2917 "Unable to negotiate a presentation context with AET \"" +
2918 parameters.GetRemoteApplicationEntityTitle() + "\"");
2919 }
2920 }
2921
2922 void Close()
2923 {
2924 if (isOpen_)
2925 {
2926 CloseInternal();
2927 }
2928 }
2929
2930 bool LookupAcceptedPresentationContext(std::map<DicomTransferSyntax, uint8_t>& target,
2931 const std::string& abstractSyntax) const
2932 {
2933 if (!IsOpen())
2934 {
2935 throw OrthancException(ErrorCode_BadSequenceOfCalls, "Connection not opened");
2936 }
2937
2938 AcceptedPresentationContexts::const_iterator found = accepted_.find(abstractSyntax);
2939
2940 if (found == accepted_.end())
2941 {
2942 return false;
2943 }
2944 else
2945 {
2946 target = found->second;
2947 return true;
2948 }
2949 }
2950
2951 void ProposeGenericPresentationContext(const std::string& abstractSyntax)
2952 {
2953 std::set<DicomTransferSyntax> ts;
2954 ts.insert(DicomTransferSyntax_LittleEndianImplicit);
2955 ts.insert(DicomTransferSyntax_LittleEndianExplicit);
2956 ts.insert(DicomTransferSyntax_BigEndianExplicit); // Retired
2957 ProposePresentationContext(abstractSyntax, ts);
2958 }
2959
2960 void ProposePresentationContext(const std::string& abstractSyntax,
2961 DicomTransferSyntax transferSyntax)
2962 {
2963 std::set<DicomTransferSyntax> ts;
2964 ts.insert(transferSyntax);
2965 ProposePresentationContext(abstractSyntax, ts);
2966 }
2967
2968 size_t GetRemainingPropositions() const
2969 {
2970 assert(proposed_.size() <= MAX_PROPOSED_PRESENTATIONS);
2971 return MAX_PROPOSED_PRESENTATIONS - proposed_.size();
2972 }
2973
2974 void ProposePresentationContext(const std::string& abstractSyntax,
2975 const std::set<DicomTransferSyntax>& transferSyntaxes)
2976 {
2977 if (transferSyntaxes.empty())
2978 {
2979 throw OrthancException(ErrorCode_ParameterOutOfRange,
2980 "No transfer syntax provided");
2981 }
2982
2983 if (proposed_.size() >= MAX_PROPOSED_PRESENTATIONS)
2984 {
2985 throw OrthancException(ErrorCode_ParameterOutOfRange,
2986 "Too many proposed presentation contexts");
2987 }
2988
2989 if (IsOpen())
2990 {
2991 Close();
2992 }
2993
2994 ProposedPresentationContext context;
2995 context.abstractSyntax_ = abstractSyntax;
2996 context.transferSyntaxes_ = transferSyntaxes;
2997
2998 proposed_.push_back(context);
2999 }
3000
3001 T_ASC_Association& GetDcmtkAssociation() const
3002 {
3003 if (isOpen_)
3004 {
3005 assert(assoc_ != NULL);
3006 return *assoc_;
3007 }
3008 else
3009 {
3010 throw OrthancException(ErrorCode_BadSequenceOfCalls,
3011 "The connection is not open");
3012 }
3013 }
3014
3015 T_ASC_Network& GetDcmtkNetwork() const
3016 {
3017 if (isOpen_)
3018 {
3019 assert(net_ != NULL);
3020 return *net_;
3021 }
3022 else
3023 {
3024 throw OrthancException(ErrorCode_BadSequenceOfCalls,
3025 "The connection is not open");
3026 }
3027 }
3028
3029 static void CheckCondition(const OFCondition& cond,
3030 const DicomAssociationParameters& parameters,
3031 const std::string& command)
3032 {
3033 if (cond.bad())
3034 {
3035 // Reformat the error message from DCMTK by turning multiline
3036 // errors into a single line
3037
3038 std::string s(cond.text());
3039 std::string info;
3040 info.reserve(s.size());
3041
3042 bool isMultiline = false;
3043 for (size_t i = 0; i < s.size(); i++)
3044 {
3045 if (s[i] == '\r')
3046 {
3047 // Ignore
3048 }
3049 else if (s[i] == '\n')
3050 {
3051 if (isMultiline)
3052 {
3053 info += "; ";
3054 }
3055 else
3056 {
3057 info += " (";
3058 isMultiline = true;
3059 }
3060 }
3061 else
3062 {
3063 info.push_back(s[i]);
3064 }
3065 }
3066
3067 if (isMultiline)
3068 {
3069 info += ")";
3070 }
3071
3072 throw OrthancException(ErrorCode_NetworkProtocol,
3073 "DicomUserConnection - " + command + " to AET \"" +
3074 parameters.GetRemoteApplicationEntityTitle() +
3075 "\": " + info);
3076 }
3077 }
3078
3079
3080 static void ReportStorageCommitment(const DicomAssociationParameters& parameters,
3081 const std::string& transactionUid,
3082 const std::vector<std::string>& sopClassUids,
3083 const std::vector<std::string>& sopInstanceUids,
3084 const std::vector<StorageCommitmentFailureReason>& failureReasons)
3085 {
3086 if (sopClassUids.size() != sopInstanceUids.size() ||
3087 sopClassUids.size() != failureReasons.size())
3088 {
3089 throw OrthancException(ErrorCode_ParameterOutOfRange);
3090 }
3091
3092
3093 std::vector<std::string> successSopClassUids, successSopInstanceUids, failedSopClassUids, failedSopInstanceUids;
3094 std::vector<StorageCommitmentFailureReason> failedReasons;
3095
3096 successSopClassUids.reserve(sopClassUids.size());
3097 successSopInstanceUids.reserve(sopClassUids.size());
3098 failedSopClassUids.reserve(sopClassUids.size());
3099 failedSopInstanceUids.reserve(sopClassUids.size());
3100 failedReasons.reserve(sopClassUids.size());
3101
3102 for (size_t i = 0; i < sopClassUids.size(); i++)
3103 {
3104 switch (failureReasons[i])
3105 {
3106 case StorageCommitmentFailureReason_Success:
3107 successSopClassUids.push_back(sopClassUids[i]);
3108 successSopInstanceUids.push_back(sopInstanceUids[i]);
3109 break;
3110
3111 case StorageCommitmentFailureReason_ProcessingFailure:
3112 case StorageCommitmentFailureReason_NoSuchObjectInstance:
3113 case StorageCommitmentFailureReason_ResourceLimitation:
3114 case StorageCommitmentFailureReason_ReferencedSOPClassNotSupported:
3115 case StorageCommitmentFailureReason_ClassInstanceConflict:
3116 case StorageCommitmentFailureReason_DuplicateTransactionUID:
3117 failedSopClassUids.push_back(sopClassUids[i]);
3118 failedSopInstanceUids.push_back(sopInstanceUids[i]);
3119 failedReasons.push_back(failureReasons[i]);
3120 break;
3121
3122 default:
3123 {
3124 char buf[16];
3125 sprintf(buf, "%04xH", failureReasons[i]);
3126 throw OrthancException(ErrorCode_ParameterOutOfRange,
3127 "Unsupported failure reason for storage commitment: " + std::string(buf));
3128 }
3129 }
3130 }
3131
3132 DicomAssociation association;
3133
3134 {
3135 std::set<DicomTransferSyntax> transferSyntaxes;
3136 transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
3137 transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
3138
3139 association.SetRole(DicomAssociationRole_Scp);
3140 association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass,
3141 transferSyntaxes);
3142 }
3143
3144 association.Open(parameters);
3145
3146 /**
3147 * N-EVENT-REPORT
3148 * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
3149 * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-1
3150 *
3151 * Status code:
3152 * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8
3153 **/
3154
3155 /**
3156 * Send the "EVENT_REPORT_RQ" request
3157 **/
3158
3159 LOG(INFO) << "Reporting modality \""
3160 << parameters.GetRemoteApplicationEntityTitle()
3161 << "\" about storage commitment transaction: " << transactionUid
3162 << " (" << successSopClassUids.size() << " successes, "
3163 << failedSopClassUids.size() << " failures)";
3164 const DIC_US messageId = association.GetDcmtkAssociation().nextMsgID++;
3165
3166 {
3167 T_DIMSE_Message message;
3168 memset(&message, 0, sizeof(message));
3169 message.CommandField = DIMSE_N_EVENT_REPORT_RQ;
3170
3171 T_DIMSE_N_EventReportRQ& content = message.msg.NEventReportRQ;
3172 content.MessageID = messageId;
3173 strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
3174 strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
3175 content.DataSetType = DIMSE_DATASET_PRESENT;
3176
3177 DcmDataset dataset;
3178 if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good())
3179 {
3180 throw OrthancException(ErrorCode_InternalError);
3181 }
3182
3183 {
3184 std::vector<StorageCommitmentFailureReason> empty;
3185 FillSopSequence(dataset, DCM_ReferencedSOPSequence, successSopClassUids,
3186 successSopInstanceUids, empty, false);
3187 }
3188
3189 // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
3190 if (failedSopClassUids.empty())
3191 {
3192 content.EventTypeID = 1; // "Storage Commitment Request Successful"
3193 }
3194 else
3195 {
3196 content.EventTypeID = 2; // "Storage Commitment Request Complete - Failures Exist"
3197
3198 // Failure reason
3199 // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part03/sect_C.14.html#sect_C.14.1.1
3200 FillSopSequence(dataset, DCM_FailedSOPSequence, failedSopClassUids,
3201 failedSopInstanceUids, failedReasons, true);
3202 }
3203
3204 int presID = ASC_findAcceptedPresentationContextID(
3205 &association.GetDcmtkAssociation(), UID_StorageCommitmentPushModelSOPClass);
3206 if (presID == 0)
3207 {
3208 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
3209 "Unable to send N-EVENT-REPORT request to AET: " +
3210 parameters.GetRemoteApplicationEntityTitle());
3211 }
3212
3213 if (!DIMSE_sendMessageUsingMemoryData(
3214 &association.GetDcmtkAssociation(), presID, &message, NULL /* status detail */,
3215 &dataset, NULL /* callback */, NULL /* callback context */,
3216 NULL /* commandSet */).good())
3217 {
3218 throw OrthancException(ErrorCode_NetworkProtocol);
3219 }
3220 }
3221
3222 /**
3223 * Read the "EVENT_REPORT_RSP" response
3224 **/
3225
3226 {
3227 T_ASC_PresentationContextID presID = 0;
3228 T_DIMSE_Message message;
3229
3230 if (!DIMSE_receiveCommand(&association.GetDcmtkAssociation(),
3231 (parameters.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
3232 parameters.GetTimeout(), &presID, &message,
3233 NULL /* no statusDetail */).good() ||
3234 message.CommandField != DIMSE_N_EVENT_REPORT_RSP)
3235 {
3236 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
3237 "Unable to read N-EVENT-REPORT response from AET: " +
3238 parameters.GetRemoteApplicationEntityTitle());
3239 }
3240
3241 const T_DIMSE_N_EventReportRSP& content = message.msg.NEventReportRSP;
3242 if (content.MessageIDBeingRespondedTo != messageId ||
3243 !(content.opts & O_NEVENTREPORT_AFFECTEDSOPCLASSUID) ||
3244 !(content.opts & O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID) ||
3245 //(content.opts & O_NEVENTREPORT_EVENTTYPEID) || // Pedantic test - The "content.EventTypeID" is not used by Orthanc
3246 std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
3247 std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance ||
3248 content.DataSetType != DIMSE_DATASET_NULL)
3249 {
3250 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
3251 "Badly formatted N-EVENT-REPORT response from AET: " +
3252 parameters.GetRemoteApplicationEntityTitle());
3253 }
3254
3255 if (content.DimseStatus != 0 /* success */)
3256 {
3257 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
3258 "The request cannot be handled by remote AET: " +
3259 parameters.GetRemoteApplicationEntityTitle());
3260 }
3261 }
3262
3263 association.Close();
3264 }
3265
3266 static void RequestStorageCommitment(const DicomAssociationParameters& parameters,
3267 const std::string& transactionUid,
3268 const std::vector<std::string>& sopClassUids,
3269 const std::vector<std::string>& sopInstanceUids)
3270 {
3271 if (sopClassUids.size() != sopInstanceUids.size())
3272 {
3273 throw OrthancException(ErrorCode_ParameterOutOfRange);
3274 }
3275
3276 for (size_t i = 0; i < sopClassUids.size(); i++)
3277 {
3278 if (sopClassUids[i].empty() ||
3279 sopInstanceUids[i].empty())
3280 {
3281 throw OrthancException(ErrorCode_ParameterOutOfRange,
3282 "The SOP class/instance UIDs cannot be empty, found: \"" +
3283 sopClassUids[i] + "\" / \"" + sopInstanceUids[i] + "\"");
3284 }
3285 }
3286
3287 if (transactionUid.size() < 5 ||
3288 transactionUid.substr(0, 5) != "2.25.")
3289 {
3290 throw OrthancException(ErrorCode_ParameterOutOfRange);
3291 }
3292
3293 DicomAssociation association;
3294
3295 {
3296 std::set<DicomTransferSyntax> transferSyntaxes;
3297 transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
3298 transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
3299
3300 association.SetRole(DicomAssociationRole_Default);
3301 association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass,
3302 transferSyntaxes);
3303 }
3304
3305 association.Open(parameters);
3306
3307 /**
3308 * N-ACTION
3309 * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html
3310 * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-4
3311 *
3312 * Status code:
3313 * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8
3314 **/
3315
3316 /**
3317 * Send the "N_ACTION_RQ" request
3318 **/
3319
3320 LOG(INFO) << "Request to modality \""
3321 << parameters.GetRemoteApplicationEntityTitle()
3322 << "\" about storage commitment for " << sopClassUids.size()
3323 << " instances, with transaction UID: " << transactionUid;
3324 const DIC_US messageId = association.GetDcmtkAssociation().nextMsgID++;
3325
3326 {
3327 T_DIMSE_Message message;
3328 memset(&message, 0, sizeof(message));
3329 message.CommandField = DIMSE_N_ACTION_RQ;
3330
3331 T_DIMSE_N_ActionRQ& content = message.msg.NActionRQ;
3332 content.MessageID = messageId;
3333 strncpy(content.RequestedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
3334 strncpy(content.RequestedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
3335 content.ActionTypeID = 1; // "Request Storage Commitment"
3336 content.DataSetType = DIMSE_DATASET_PRESENT;
3337
3338 DcmDataset dataset;
3339 if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good())
3340 {
3341 throw OrthancException(ErrorCode_InternalError);
3342 }
3343
3344 {
3345 std::vector<StorageCommitmentFailureReason> empty;
3346 FillSopSequence(dataset, DCM_ReferencedSOPSequence, sopClassUids, sopInstanceUids, empty, false);
3347 }
3348
3349 int presID = ASC_findAcceptedPresentationContextID(
3350 &association.GetDcmtkAssociation(), UID_StorageCommitmentPushModelSOPClass);
3351 if (presID == 0)
3352 {
3353 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
3354 "Unable to send N-ACTION request to AET: " +
3355 parameters.GetRemoteApplicationEntityTitle());
3356 }
3357
3358 if (!DIMSE_sendMessageUsingMemoryData(
3359 &association.GetDcmtkAssociation(), presID, &message, NULL /* status detail */,
3360 &dataset, NULL /* callback */, NULL /* callback context */,
3361 NULL /* commandSet */).good())
3362 {
3363 throw OrthancException(ErrorCode_NetworkProtocol);
3364 }
3365 }
3366
3367 /**
3368 * Read the "N_ACTION_RSP" response
3369 **/
3370
3371 {
3372 T_ASC_PresentationContextID presID = 0;
3373 T_DIMSE_Message message;
3374
3375 if (!DIMSE_receiveCommand(&association.GetDcmtkAssociation(),
3376 (parameters.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
3377 parameters.GetTimeout(), &presID, &message,
3378 NULL /* no statusDetail */).good() ||
3379 message.CommandField != DIMSE_N_ACTION_RSP)
3380 {
3381 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
3382 "Unable to read N-ACTION response from AET: " +
3383 parameters.GetRemoteApplicationEntityTitle());
3384 }
3385
3386 const T_DIMSE_N_ActionRSP& content = message.msg.NActionRSP;
3387 if (content.MessageIDBeingRespondedTo != messageId ||
3388 !(content.opts & O_NACTION_AFFECTEDSOPCLASSUID) ||
3389 !(content.opts & O_NACTION_AFFECTEDSOPINSTANCEUID) ||
3390 //(content.opts & O_NACTION_ACTIONTYPEID) || // Pedantic test - The "content.ActionTypeID" is not used by Orthanc
3391 std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
3392 std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance ||
3393 content.DataSetType != DIMSE_DATASET_NULL)
3394 {
3395 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
3396 "Badly formatted N-ACTION response from AET: " +
3397 parameters.GetRemoteApplicationEntityTitle());
3398 }
3399
3400 if (content.DimseStatus != 0 /* success */)
3401 {
3402 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
3403 "The request cannot be handled by remote AET: " +
3404 parameters.GetRemoteApplicationEntityTitle());
3405 }
3406 }
3407
3408 association.Close();
3409 }
3410 };
3411
3412
3413
3414 static void TestAndCopyTag(DicomMap& result,
3415 const DicomMap& source,
3416 const DicomTag& tag)
3417 {
3418 if (!source.HasTag(tag))
3419 {
3420 throw OrthancException(ErrorCode_BadRequest);
3421 }
3422 else
3423 {
3424 result.SetValue(tag, source.GetValue(tag));
3425 }
3426 }
3427
3428
3429 namespace
3430 {
3431 struct FindPayload
3432 {
3433 DicomFindAnswers* answers;
3434 const char* level;
3435 bool isWorklist;
3436 };
3437 }
3438
3439
3440 static void FindCallback(
3441 /* in */
3442 void *callbackData,
3443 T_DIMSE_C_FindRQ *request, /* original find request */
3444 int responseCount,
3445 T_DIMSE_C_FindRSP *response, /* pending response received */
3446 DcmDataset *responseIdentifiers /* pending response identifiers */
3447 )
3448 {
3449 FindPayload& payload = *reinterpret_cast<FindPayload*>(callbackData);
3450
3451 if (responseIdentifiers != NULL)
3452 {
3453 if (payload.isWorklist)
3454 {
3455 ParsedDicomFile answer(*responseIdentifiers);
3456 payload.answers->Add(answer);
3457 }
3458 else
3459 {
3460 DicomMap m;
3461 FromDcmtkBridge::ExtractDicomSummary(m, *responseIdentifiers);
3462
3463 if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
3464 {
3465 m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level, false);
3466 }
3467
3468 payload.answers->Add(m);
3469 }
3470 }
3471 }
3472
3473
3474 static void NormalizeFindQuery(DicomMap& fixedQuery,
3475 ResourceType level,
3476 const DicomMap& fields)
3477 {
3478 std::set<DicomTag> allowedTags;
3479
3480 // WARNING: Do not add "break" or reorder items in this switch-case!
3481 switch (level)
3482 {
3483 case ResourceType_Instance:
3484 DicomTag::AddTagsForModule(allowedTags, DicomModule_Instance);
3485
3486 case ResourceType_Series:
3487 DicomTag::AddTagsForModule(allowedTags, DicomModule_Series);
3488
3489 case ResourceType_Study:
3490 DicomTag::AddTagsForModule(allowedTags, DicomModule_Study);
3491
3492 case ResourceType_Patient:
3493 DicomTag::AddTagsForModule(allowedTags, DicomModule_Patient);
3494 break;
3495
3496 default:
3497 throw OrthancException(ErrorCode_InternalError);
3498 }
3499
3500 switch (level)
3501 {
3502 case ResourceType_Patient:
3503 allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES);
3504 allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES);
3505 allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES);
3506 break;
3507
3508 case ResourceType_Study:
3509 allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY);
3510 allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES);
3511 allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES);
3512 allowedTags.insert(DICOM_TAG_SOP_CLASSES_IN_STUDY);
3513 break;
3514
3515 case ResourceType_Series:
3516 allowedTags.insert(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES);
3517 break;
3518
3519 default:
3520 break;
3521 }
3522
3523 allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET);
3524
3525 DicomArray query(fields);
3526 for (size_t i = 0; i < query.GetSize(); i++)
3527 {
3528 const DicomTag& tag = query.GetElement(i).GetTag();
3529 if (allowedTags.find(tag) == allowedTags.end())
3530 {
3531 LOG(WARNING) << "Tag not allowed for this C-Find level, will be ignored: " << tag;
3532 }
3533 else
3534 {
3535 fixedQuery.SetValue(tag, query.GetElement(i).GetValue());
3536 }
3537 }
3538 }
3539
3540
3541
3542 static ParsedDicomFile* ConvertQueryFields(const DicomMap& fields,
3543 ModalityManufacturer manufacturer)
3544 {
3545 // Fix outgoing C-Find requests issue for Syngo.Via and its
3546 // solution was reported by Emsy Chan by private mail on
3547 // 2015-06-17. According to Robert van Ommen (2015-11-30), the
3548 // same fix is required for Agfa Impax. This was generalized for
3549 // generic manufacturer since it seems to affect PhilipsADW,
3550 // GEWAServer as well:
3551 // https://bitbucket.org/sjodogne/orthanc/issues/31/
3552
3553 switch (manufacturer)
3554 {
3555 case ModalityManufacturer_GenericNoWildcardInDates:
3556 case ModalityManufacturer_GenericNoUniversalWildcard:
3557 {
3558 std::unique_ptr<DicomMap> fix(fields.Clone());
3559
3560 std::set<DicomTag> tags;
3561 fix->GetTags(tags);
3562
3563 for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
3564 {
3565 // Replace a "*" wildcard query by an empty query ("") for
3566 // "date" or "all" value representations depending on the
3567 // type of manufacturer.
3568 if (manufacturer == ModalityManufacturer_GenericNoUniversalWildcard ||
3569 (manufacturer == ModalityManufacturer_GenericNoWildcardInDates &&
3570 FromDcmtkBridge::LookupValueRepresentation(*it) == ValueRepresentation_Date))
3571 {
3572 const DicomValue* value = fix->TestAndGetValue(*it);
3573
3574 if (value != NULL &&
3575 !value->IsNull() &&
3576 value->GetContent() == "*")
3577 {
3578 fix->SetValue(*it, "", false);
3579 }
3580 }
3581 }
3582
3583 return new ParsedDicomFile(*fix, GetDefaultDicomEncoding(), false /* be strict */);
3584 }
3585
3586 default:
3587 return new ParsedDicomFile(fields, GetDefaultDicomEncoding(), false /* be strict */);
3588 }
3589 }
3590
3591
3592
3593 class DicomControlUserConnection : public boost::noncopyable
3594 {
3595 private:
3596 DicomAssociationParameters parameters_;
3597 DicomAssociation association_;
3598
3599 void SetupPresentationContexts()
3600 {
3601 association_.ProposeGenericPresentationContext(UID_VerificationSOPClass);
3602 association_.ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel);
3603 association_.ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel);
3604 association_.ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel);
3605 association_.ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel);
3606 }
3607
3608 void FindInternal(DicomFindAnswers& answers,
3609 DcmDataset* dataset,
3610 const char* sopClass,
3611 bool isWorklist,
3612 const char* level)
3613 {
3614 assert(isWorklist ^ (level != NULL));
3615
3616 association_.Open(parameters_);
3617
3618 FindPayload payload;
3619 payload.answers = &answers;
3620 payload.level = level;
3621 payload.isWorklist = isWorklist;
3622
3623 // Figure out which of the accepted presentation contexts should be used
3624 int presID = ASC_findAcceptedPresentationContextID(
3625 &association_.GetDcmtkAssociation(), sopClass);
3626 if (presID == 0)
3627 {
3628 throw OrthancException(ErrorCode_DicomFindUnavailable,
3629 "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle());
3630 }
3631
3632 T_DIMSE_C_FindRQ request;
3633 memset(&request, 0, sizeof(request));
3634 request.MessageID = association_.GetDcmtkAssociation().nextMsgID++;
3635 strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
3636 request.Priority = DIMSE_PRIORITY_MEDIUM;
3637 request.DataSetType = DIMSE_DATASET_PRESENT;
3638
3639 T_DIMSE_C_FindRSP response;
3640 DcmDataset* statusDetail = NULL;
3641
3642 #if DCMTK_VERSION_NUMBER >= 364
3643 int responseCount;
3644 #endif
3645
3646 OFCondition cond = DIMSE_findUser(
3647 &association_.GetDcmtkAssociation(), presID, &request, dataset,
3648 #if DCMTK_VERSION_NUMBER >= 364
3649 responseCount,
3650 #endif
3651 FindCallback, &payload,
3652 /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
3653 /*opt_dimse_timeout*/ parameters_.GetTimeout(),
3654 &response, &statusDetail);
3655
3656 if (statusDetail)
3657 {
3658 delete statusDetail;
3659 }
3660
3661 DicomAssociation::CheckCondition(cond, parameters_, "C-FIND");
3662
3663
3664 /**
3665 * New in Orthanc 1.6.0: Deal with failures during C-FIND.
3666 * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#table_C.4-1
3667 **/
3668
3669 if (response.DimseStatus != 0x0000 && // Success
3670 response.DimseStatus != 0xFF00 && // Pending - Matches are continuing
3671 response.DimseStatus != 0xFF01) // Pending - Matches are continuing
3672 {
3673 char buf[16];
3674 sprintf(buf, "%04X", response.DimseStatus);
3675
3676 if (response.DimseStatus == STATUS_FIND_Failed_UnableToProcess)
3677 {
3678 throw OrthancException(ErrorCode_NetworkProtocol,
3679 HttpStatus_422_UnprocessableEntity,
3680 "C-FIND SCU to AET \"" +
3681 parameters_.GetRemoteApplicationEntityTitle() +
3682 "\" has failed with DIMSE status 0x" + buf +
3683 " (unable to process - invalid query ?)");
3684 }
3685 else
3686 {
3687 throw OrthancException(ErrorCode_NetworkProtocol, "C-FIND SCU to AET \"" +
3688 parameters_.GetRemoteApplicationEntityTitle() +
3689 "\" has failed with DIMSE status 0x" + buf);
3690 }
3691 }
3692 }
3693
3694 void MoveInternal(const std::string& targetAet,
3695 ResourceType level,
3696 const DicomMap& fields)
3697 {
3698 association_.Open(parameters_);
3699
3700 std::unique_ptr<ParsedDicomFile> query(
3701 ConvertQueryFields(fields, parameters_.GetRemoteManufacturer()));
3702 DcmDataset* dataset = query->GetDcmtkObject().getDataset();
3703
3704 const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel;
3705 switch (level)
3706 {
3707 case ResourceType_Patient:
3708 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT");
3709 break;
3710
3711 case ResourceType_Study:
3712 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY");
3713 break;
3714
3715 case ResourceType_Series:
3716 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES");
3717 break;
3718
3719 case ResourceType_Instance:
3720 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE");
3721 break;
3722
3723 default:
3724 throw OrthancException(ErrorCode_ParameterOutOfRange);
3725 }
3726
3727 // Figure out which of the accepted presentation contexts should be used
3728 int presID = ASC_findAcceptedPresentationContextID(&association_.GetDcmtkAssociation(), sopClass);
3729 if (presID == 0)
3730 {
3731 throw OrthancException(ErrorCode_DicomMoveUnavailable,
3732 "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle());
3733 }
3734
3735 T_DIMSE_C_MoveRQ request;
3736 memset(&request, 0, sizeof(request));
3737 request.MessageID = association_.GetDcmtkAssociation().nextMsgID++;
3738 strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
3739 request.Priority = DIMSE_PRIORITY_MEDIUM;
3740 request.DataSetType = DIMSE_DATASET_PRESENT;
3741 strncpy(request.MoveDestination, targetAet.c_str(), DIC_AE_LEN);
3742
3743 T_DIMSE_C_MoveRSP response;
3744 DcmDataset* statusDetail = NULL;
3745 DcmDataset* responseIdentifiers = NULL;
3746 OFCondition cond = DIMSE_moveUser(
3747 &association_.GetDcmtkAssociation(), presID, &request, dataset, NULL, NULL,
3748 /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
3749 /*opt_dimse_timeout*/ parameters_.GetTimeout(),
3750 &association_.GetDcmtkNetwork(), NULL, NULL,
3751 &response, &statusDetail, &responseIdentifiers);
3752
3753 if (statusDetail)
3754 {
3755 delete statusDetail;
3756 }
3757
3758 if (responseIdentifiers)
3759 {
3760 delete responseIdentifiers;
3761 }
3762
3763 DicomAssociation::CheckCondition(cond, parameters_, "C-MOVE");
3764
3765
3766 /**
3767 * New in Orthanc 1.6.0: Deal with failures during C-MOVE.
3768 * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.2.html#table_C.4-2
3769 **/
3770
3771 if (response.DimseStatus != 0x0000 && // Success
3772 response.DimseStatus != 0xFF00) // Pending - Sub-operations are continuing
3773 {
3774 char buf[16];
3775 sprintf(buf, "%04X", response.DimseStatus);
3776
3777 if (response.DimseStatus == STATUS_MOVE_Failed_UnableToProcess)
3778 {
3779 throw OrthancException(ErrorCode_NetworkProtocol,
3780 HttpStatus_422_UnprocessableEntity,
3781 "C-MOVE SCU to AET \"" +
3782 parameters_.GetRemoteApplicationEntityTitle() +
3783 "\" has failed with DIMSE status 0x" + buf +
3784 " (unable to process - resource not found ?)");
3785 }
3786 else
3787 {
3788 throw OrthancException(ErrorCode_NetworkProtocol, "C-MOVE SCU to AET \"" +
3789 parameters_.GetRemoteApplicationEntityTitle() +
3790 "\" has failed with DIMSE status 0x" + buf);
3791 }
3792 }
3793 }
3794
3795 public:
3796 DicomControlUserConnection(const DicomAssociationParameters& params) :
3797 parameters_(params)
3798 {
3799 SetupPresentationContexts();
3800 }
3801
3802 const DicomAssociationParameters& GetParameters() const
3803 {
3804 return parameters_;
3805 }
3806
3807 bool Echo()
3808 {
3809 association_.Open(parameters_);
3810
3811 DIC_US status;
3812 DicomAssociation::CheckCondition(
3813 DIMSE_echoUser(&association_.GetDcmtkAssociation(),
3814 association_.GetDcmtkAssociation().nextMsgID++,
3815 /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
3816 /*opt_dimse_timeout*/ parameters_.GetTimeout(),
3817 &status, NULL),
3818 parameters_, "C-ECHO");
3819
3820 return status == STATUS_Success;
3821 }
3822
3823
3824 void Find(DicomFindAnswers& result,
3825 ResourceType level,
3826 const DicomMap& originalFields,
3827 bool normalize)
3828 {
3829 std::unique_ptr<ParsedDicomFile> query;
3830
3831 if (normalize)
3832 {
3833 DicomMap fields;
3834 NormalizeFindQuery(fields, level, originalFields);
3835 query.reset(ConvertQueryFields(fields, parameters_.GetRemoteManufacturer()));
3836 }
3837 else
3838 {
3839 query.reset(new ParsedDicomFile(originalFields,
3840 GetDefaultDicomEncoding(),
3841 false /* be strict */));
3842 }
3843
3844 DcmDataset* dataset = query->GetDcmtkObject().getDataset();
3845
3846 const char* clevel = NULL;
3847 const char* sopClass = NULL;
3848
3849 switch (level)
3850 {
3851 case ResourceType_Patient:
3852 clevel = "PATIENT";
3853 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT");
3854 sopClass = UID_FINDPatientRootQueryRetrieveInformationModel;
3855 break;
3856
3857 case ResourceType_Study:
3858 clevel = "STUDY";
3859 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY");
3860 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
3861 break;
3862
3863 case ResourceType_Series:
3864 clevel = "SERIES";
3865 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES");
3866 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
3867 break;
3868
3869 case ResourceType_Instance:
3870 clevel = "IMAGE";
3871 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE");
3872 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
3873 break;
3874
3875 default:
3876 throw OrthancException(ErrorCode_ParameterOutOfRange);
3877 }
3878
3879
3880 const char* universal;
3881 if (parameters_.GetRemoteManufacturer() == ModalityManufacturer_GE)
3882 {
3883 universal = "*";
3884 }
3885 else
3886 {
3887 universal = "";
3888 }
3889
3890
3891 // Add the expected tags for this query level.
3892 // WARNING: Do not reorder or add "break" in this switch-case!
3893 switch (level)
3894 {
3895 case ResourceType_Instance:
3896 if (!dataset->tagExists(DCM_SOPInstanceUID))
3897 {
3898 DU_putStringDOElement(dataset, DCM_SOPInstanceUID, universal);
3899 }
3900
3901 case ResourceType_Series:
3902 if (!dataset->tagExists(DCM_SeriesInstanceUID))
3903 {
3904 DU_putStringDOElement(dataset, DCM_SeriesInstanceUID, universal);
3905 }
3906
3907 case ResourceType_Study:
3908 if (!dataset->tagExists(DCM_AccessionNumber))
3909 {
3910 DU_putStringDOElement(dataset, DCM_AccessionNumber, universal);
3911 }
3912
3913 if (!dataset->tagExists(DCM_StudyInstanceUID))
3914 {
3915 DU_putStringDOElement(dataset, DCM_StudyInstanceUID, universal);
3916 }
3917
3918 case ResourceType_Patient:
3919 if (!dataset->tagExists(DCM_PatientID))
3920 {
3921 DU_putStringDOElement(dataset, DCM_PatientID, universal);
3922 }
3923
3924 break;
3925
3926 default:
3927 throw OrthancException(ErrorCode_ParameterOutOfRange);
3928 }
3929
3930 assert(clevel != NULL && sopClass != NULL);
3931 FindInternal(result, dataset, sopClass, false, clevel);
3932 }
3933
3934
3935 void Move(const std::string& targetAet,
3936 ResourceType level,
3937 const DicomMap& findResult)
3938 {
3939 DicomMap move;
3940 switch (level)
3941 {
3942 case ResourceType_Patient:
3943 TestAndCopyTag(move, findResult, DICOM_TAG_PATIENT_ID);
3944 break;
3945
3946 case ResourceType_Study:
3947 TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
3948 break;
3949
3950 case ResourceType_Series:
3951 TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
3952 TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
3953 break;
3954
3955 case ResourceType_Instance:
3956 TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
3957 TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
3958 TestAndCopyTag(move, findResult, DICOM_TAG_SOP_INSTANCE_UID);
3959 break;
3960
3961 default:
3962 throw OrthancException(ErrorCode_InternalError);
3963 }
3964
3965 MoveInternal(targetAet, level, move);
3966 }
3967
3968
3969 void Move(const std::string& targetAet,
3970 const DicomMap& findResult)
3971 {
3972 if (!findResult.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
3973 {
3974 throw OrthancException(ErrorCode_InternalError);
3975 }
3976
3977 const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent();
3978 ResourceType level = StringToResourceType(tmp.c_str());
3979
3980 Move(targetAet, level, findResult);
3981 }
3982
3983
3984 void MovePatient(const std::string& targetAet,
3985 const std::string& patientId)
3986 {
3987 DicomMap query;
3988 query.SetValue(DICOM_TAG_PATIENT_ID, patientId, false);
3989 MoveInternal(targetAet, ResourceType_Patient, query);
3990 }
3991
3992 void MoveStudy(const std::string& targetAet,
3993 const std::string& studyUid)
3994 {
3995 DicomMap query;
3996 query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
3997 MoveInternal(targetAet, ResourceType_Study, query);
3998 }
3999
4000 void MoveSeries(const std::string& targetAet,
4001 const std::string& studyUid,
4002 const std::string& seriesUid)
4003 {
4004 DicomMap query;
4005 query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
4006 query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false);
4007 MoveInternal(targetAet, ResourceType_Series, query);
4008 }
4009
4010 void MoveInstance(const std::string& targetAet,
4011 const std::string& studyUid,
4012 const std::string& seriesUid,
4013 const std::string& instanceUid)
4014 {
4015 DicomMap query;
4016 query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
4017 query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false);
4018 query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, false);
4019 MoveInternal(targetAet, ResourceType_Instance, query);
4020 }
4021
4022
4023 void FindWorklist(DicomFindAnswers& result,
4024 ParsedDicomFile& query)
4025 {
4026 DcmDataset* dataset = query.GetDcmtkObject().getDataset();
4027 const char* sopClass = UID_FINDModalityWorklistInformationModel;
4028
4029 FindInternal(result, dataset, sopClass, true, NULL);
4030 }
4031 };
4032
4033
4034 class DicomStoreUserConnection : public boost::noncopyable
4035 {
4036 private:
4037 typedef std::map<std::string, std::set<DicomTransferSyntax> > StorageClasses;
4038
4039 DicomAssociationParameters parameters_;
4040 DicomAssociation association_;
4041 StorageClasses storageClasses_;
4042 bool proposeCommonClasses_;
4043 bool proposeUncompressedSyntaxes_;
4044 bool proposeRetiredBigEndian_;
4045
4046
4047 /**
4048
4049 Orthanc < 1.7.0:
4050
4051 Input | Output
4052 -------------+---------------------------------------------
4053 Compressed | Same transfer syntax
4054 Uncompressed | Same transfer syntax, or other uncompressed
4055
4056 Orthanc >= 1.7.0:
4057
4058 Input | Output
4059 -------------+---------------------------------------------
4060 Compressed | Same transfer syntax, or uncompressed
4061 Uncompressed | Same transfer syntax, or other uncompressed
4062
4063 **/
4064
4065
4066 // Return "false" if there is not enough room remaining in the association
4067 bool ProposeStorageClass(const std::string& sopClassUid,
4068 const std::set<DicomTransferSyntax>& syntaxes)
4069 {
4070 size_t requiredCount = syntaxes.size();
4071 if (proposeUncompressedSyntaxes_)
4072 {
4073 requiredCount += 1;
4074 }
4075
4076 if (association_.GetRemainingPropositions() <= requiredCount)
4077 {
4078 return false; // Not enough room
4079 }
4080
4081 for (std::set<DicomTransferSyntax>::const_iterator
4082 it = syntaxes.begin(); it != syntaxes.end(); ++it)
4083 {
4084 association_.ProposePresentationContext(sopClassUid, *it);
4085 }
4086
4087 if (proposeUncompressedSyntaxes_)
4088 {
4089 std::set<DicomTransferSyntax> uncompressed;
4090
4091 if (syntaxes.find(DicomTransferSyntax_LittleEndianImplicit) == syntaxes.end())
4092 {
4093 uncompressed.insert(DicomTransferSyntax_LittleEndianImplicit);
4094 }
4095
4096 if (syntaxes.find(DicomTransferSyntax_LittleEndianExplicit) == syntaxes.end())
4097 {
4098 uncompressed.insert(DicomTransferSyntax_LittleEndianExplicit);
4099 }
4100
4101 if (proposeRetiredBigEndian_ &&
4102 syntaxes.find(DicomTransferSyntax_BigEndianExplicit) == syntaxes.end())
4103 {
4104 uncompressed.insert(DicomTransferSyntax_BigEndianExplicit);
4105 }
4106
4107 if (!uncompressed.empty())
4108 {
4109 association_.ProposePresentationContext(sopClassUid, uncompressed);
4110 }
4111 }
4112
4113 return true;
4114 }
4115
4116
4117 bool LookupPresentationContext(uint8_t& presentationContextId,
4118 const std::string& sopClassUid,
4119 DicomTransferSyntax transferSyntax)
4120 {
4121 typedef std::map<DicomTransferSyntax, uint8_t> PresentationContexts;
4122
4123 PresentationContexts pc;
4124 if (association_.IsOpen() &&
4125 association_.LookupAcceptedPresentationContext(pc, sopClassUid))
4126 {
4127 PresentationContexts::const_iterator found = pc.find(transferSyntax);
4128 if (found != pc.end())
4129 {
4130 presentationContextId = found->second;
4131 return true;
4132 }
4133 }
4134
4135 return false;
4136 }
4137
4138
4139 bool NegotiatePresentationContext(uint8_t& presentationContextId,
4140 const std::string& sopClassUid,
4141 DicomTransferSyntax transferSyntax)
4142 {
4143 /**
4144 * Step 1: Check whether this presentation context is already
4145 * available in the previously negociated assocation.
4146 **/
4147
4148 if (LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax))
4149 {
4150 return true;
4151 }
4152
4153 // The association must be re-negotiated
4154 association_.ClearPresentationContexts();
4155 PrepareStorageClass(sopClassUid, transferSyntax);
4156
4157
4158 /**
4159 * Step 2: Propose at least the mandatory SOP class.
4160 **/
4161
4162 {
4163 StorageClasses::const_iterator mandatory = storageClasses_.find(sopClassUid);
4164
4165 if (mandatory == storageClasses_.end() ||
4166 mandatory->second.find(transferSyntax) == mandatory->second.end())
4167 {
4168 throw OrthancException(ErrorCode_InternalError);
4169 }
4170
4171 if (!ProposeStorageClass(sopClassUid, mandatory->second))
4172 {
4173 // Should never happen in real life: There are no more than
4174 // 128 transfer syntaxes in DICOM!
4175 throw OrthancException(ErrorCode_InternalError,
4176 "Too many transfer syntaxes for SOP class UID: " + sopClassUid);
4177 }
4178 }
4179
4180
4181 /**
4182 * Step 3: Propose all the previously spotted SOP classes, as
4183 * registered through the "PrepareStorageClass()" method.
4184 **/
4185
4186 for (StorageClasses::const_iterator it = storageClasses_.begin();
4187 it != storageClasses_.end(); ++it)
4188 {
4189 if (it->first != sopClassUid)
4190 {
4191 ProposeStorageClass(it->first, it->second);
4192 }
4193 }
4194
4195
4196 /**
4197 * Step 4: As long as there is room left in the proposed
4198 * presentation contexts, propose the uncompressed transfer syntaxes
4199 * for the most common SOP classes, as can be found in the
4200 * "dcmShortSCUStorageSOPClassUIDs" array from DCMTK. The
4201 * preferred transfer syntax is "LittleEndianImplicit".
4202 **/
4203
4204 if (proposeCommonClasses_)
4205 {
4206 std::set<DicomTransferSyntax> ts;
4207 ts.insert(DicomTransferSyntax_LittleEndianImplicit);
4208
4209 for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs; i++)
4210 {
4211 std::string c(dcmShortSCUStorageSOPClassUIDs[i]);
4212
4213 if (c != sopClassUid &&
4214 storageClasses_.find(c) == storageClasses_.end())
4215 {
4216 ProposeStorageClass(c, ts);
4217 }
4218 }
4219 }
4220
4221
4222 /**
4223 * Step 5: Open the association, and check whether the pair (SOP
4224 * class UID, transfer syntax) was accepted by the remote host.
4225 **/
4226
4227 association_.Open(parameters_);
4228 return LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax);
4229 }
4230
4231 public:
4232 DicomStoreUserConnection(const DicomAssociationParameters& params) :
4233 parameters_(params),
4234 proposeCommonClasses_(true),
4235 proposeUncompressedSyntaxes_(true),
4236 proposeRetiredBigEndian_(false)
4237 {
4238 }
4239
4240 const DicomAssociationParameters& GetParameters() const
4241 {
4242 return parameters_;
4243 }
4244
4245 void SetCommonClassesProposed(bool proposed)
4246 {
4247 proposeCommonClasses_ = proposed;
4248 }
4249
4250 bool IsCommonClassesProposed() const
4251 {
4252 return proposeCommonClasses_;
4253 }
4254
4255 void SetUncompressedSyntaxesProposed(bool proposed)
4256 {
4257 proposeUncompressedSyntaxes_ = proposed;
4258 }
4259
4260 bool IsUncompressedSyntaxesProposed() const
4261 {
4262 return proposeUncompressedSyntaxes_;
4263 }
4264
4265 void SetRetiredBigEndianProposed(bool propose)
4266 {
4267 proposeRetiredBigEndian_ = propose;
4268 }
4269
4270 bool IsRetiredBigEndianProposed() const
4271 {
4272 return proposeRetiredBigEndian_;
4273 }
4274
4275 void PrepareStorageClass(const std::string& sopClassUid,
4276 DicomTransferSyntax syntax)
4277 {
4278 StorageClasses::iterator found = storageClasses_.find(sopClassUid);
4279
4280 if (found == storageClasses_.end())
4281 {
4282 std::set<DicomTransferSyntax> ts;
4283 ts.insert(syntax);
4284 storageClasses_[sopClassUid] = ts;
4285 }
4286 else
4287 {
4288 found->second.insert(syntax);
4289 }
4290 }
4291
4292
4293 void Toto(const std::string& sopClassUid,
4294 DicomTransferSyntax transferSyntax)
4295 {
4296 uint8_t id;
4297
4298 if (NegotiatePresentationContext(id, sopClassUid, transferSyntax))
4299 {
4300 printf("**** OK, without transcoding !! %d\n", id);
4301 }
4302 else
4303 {
4304 // Transcoding - only in Orthanc >= 1.7.0
4305
4306 const DicomTransferSyntax uncompressed[] = {
4307 DicomTransferSyntax_LittleEndianImplicit, // Default transfer syntax
4308 DicomTransferSyntax_LittleEndianExplicit,
4309 DicomTransferSyntax_BigEndianExplicit
4310 };
4311
4312 bool found = false;
4313 for (size_t i = 0; i < 3; i++)
4314 {
4315 if (LookupPresentationContext(id, sopClassUid, uncompressed[i]))
4316 {
4317 printf("**** TRANSCODING to %s => %d\n",
4318 GetTransferSyntaxUid(uncompressed[i]), id);
4319 found = true;
4320 break;
4321 }
4322 }
4323
4324 if (!found)
4325 {
4326 printf("**** KO KO KO\n");
4327 }
4328 }
4329 }
4330 };
4331 }
4332
4333 2412
4334 TEST(Toto, DISABLED_DicomAssociation) 2413 TEST(Toto, DISABLED_DicomAssociation)
4335 { 2414 {
4336 DicomAssociationParameters params; 2415 DicomAssociationParameters params;
4337 params.SetLocalApplicationEntityTitle("ORTHANC"); 2416 params.SetLocalApplicationEntityTitle("ORTHANC");
4384 } 2463 }
4385 2464
4386 #endif 2465 #endif
4387 } 2466 }
4388 2467
2468 static void TestTranscode(DicomStoreUserConnection& scu,
2469 const std::string& sopClassUid,
2470 DicomTransferSyntax transferSyntax)
2471 {
2472 uint8_t id;
2473
2474 if (scu.NegotiatePresentationContext(id, sopClassUid, transferSyntax))
2475 {
2476 printf("**** OK, without transcoding !! %d\n", id);
2477 }
2478 else
2479 {
2480 // Transcoding - only in Orthanc >= 1.7.0
2481
2482 const DicomTransferSyntax uncompressed[] = {
2483 DicomTransferSyntax_LittleEndianImplicit, // Default transfer syntax
2484 DicomTransferSyntax_LittleEndianExplicit,
2485 DicomTransferSyntax_BigEndianExplicit
2486 };
2487
2488 bool found = false;
2489 for (size_t i = 0; i < 3; i++)
2490 {
2491 if (scu.LookupPresentationContext(id, sopClassUid, uncompressed[i]))
2492 {
2493 printf("**** TRANSCODING to %s => %d\n",
2494 GetTransferSyntaxUid(uncompressed[i]), id);
2495 found = true;
2496 break;
2497 }
2498 }
2499
2500 if (!found)
2501 {
2502 printf("**** KO KO KO\n");
2503 }
2504 }
2505 }
2506
4389 2507
4390 TEST(Toto, DISABLED_Store) 2508 TEST(Toto, DISABLED_Store)
4391 { 2509 {
4392 DicomAssociationParameters params; 2510 DicomAssociationParameters params;
4393 params.SetLocalApplicationEntityTitle("ORTHANC"); 2511 params.SetLocalApplicationEntityTitle("ORTHANC");
4399 assoc.PrepareStorageClass(UID_MRImageStorage, DicomTransferSyntax_JPEGProcess2_4); 2517 assoc.PrepareStorageClass(UID_MRImageStorage, DicomTransferSyntax_JPEGProcess2_4);
4400 //assoc.PrepareStorageClass(UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit); 2518 //assoc.PrepareStorageClass(UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit);
4401 2519
4402 //assoc.SetUncompressedSyntaxesProposed(false); 2520 //assoc.SetUncompressedSyntaxesProposed(false);
4403 //assoc.SetCommonClassesProposed(false); 2521 //assoc.SetCommonClassesProposed(false);
4404 assoc.Toto(UID_MRImageStorage, DicomTransferSyntax_JPEG2000); 2522 TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_JPEG2000);
4405 //assoc.Toto(UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit); 2523 //TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit);
4406 } 2524 }
4407 2525
4408 #endif 2526 #endif