Mercurial > hg > orthanc
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(¶ms_); | |
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(¶ms_, /*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 = ¶ms_->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 |