comparison OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp @ 4712:bad8935cd5f2

"/studies/{id}/split" accepts "Instances" parameter to split instances instead of series
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 23 Jun 2021 14:33:20 +0200
parents facea16b055b
children fb98db281d1d
comparison
equal deleted inserted replaced
4707:81ad9d8a0fa6 4712:bad8935cd5f2
48 48
49 #define INFO_SUBSEQUENCES \ 49 #define INFO_SUBSEQUENCES \
50 "Starting with Orthanc 1.9.4, paths to subsequences can be provided using the "\ 50 "Starting with Orthanc 1.9.4, paths to subsequences can be provided using the "\
51 "same syntax as the `dcmodify` command-line tool (wildcards are supported as well)." 51 "same syntax as the `dcmodify` command-line tool (wildcards are supported as well)."
52 52
53
54 static const char* const CONTENT = "Content";
55 static const char* const FORCE = "Force";
56 static const char* const INSTANCES = "Instances";
57 static const char* const INTERPRET_BINARY_TAGS = "InterpretBinaryTags";
58 static const char* const KEEP = "Keep";
59 static const char* const KEEP_PRIVATE_TAGS = "KeepPrivateTags";
60 static const char* const KEEP_SOURCE = "KeepSource";
61 static const char* const PARENT = "Parent";
62 static const char* const PRIVATE_CREATOR = "PrivateCreator";
63 static const char* const REMOVE = "Remove";
64 static const char* const REPLACE = "Replace";
65 static const char* const RESOURCES = "Resources";
66 static const char* const SERIES = "Series";
67 static const char* const TAGS = "Tags";
68 static const char* const TRANSCODE = "Transcode";
69
70
53 namespace Orthanc 71 namespace Orthanc
54 { 72 {
55 // Modification of DICOM instances ------------------------------------------ 73 // Modification of DICOM instances ------------------------------------------
56 74
57 75
64 82
65 static void DocumentModifyOptions(RestApiPostCall& call) 83 static void DocumentModifyOptions(RestApiPostCall& call)
66 { 84 {
67 // Check out "DicomModification::ParseModifyRequest()" 85 // Check out "DicomModification::ParseModifyRequest()"
68 call.GetDocumentation() 86 call.GetDocumentation()
69 .SetRequestField("Transcode", RestApiCallDocumentation::Type_String, 87 .SetRequestField(TRANSCODE, RestApiCallDocumentation::Type_String,
70 "Transcode the DICOM instances to the provided DICOM transfer syntax: " 88 "Transcode the DICOM instances to the provided DICOM transfer syntax: "
71 "https://book.orthanc-server.com/faq/transcoding.html", false) 89 "https://book.orthanc-server.com/faq/transcoding.html", false)
72 .SetRequestField("Force", RestApiCallDocumentation::Type_Boolean, 90 .SetRequestField(FORCE, RestApiCallDocumentation::Type_Boolean,
73 "Allow the modification of tags related to DICOM identifiers, at the risk of " 91 "Allow the modification of tags related to DICOM identifiers, at the risk of "
74 "breaking the DICOM model of the real world", false) 92 "breaking the DICOM model of the real world", false)
75 .SetRequestField("RemovePrivateTags", RestApiCallDocumentation::Type_Boolean, 93 .SetRequestField("RemovePrivateTags", RestApiCallDocumentation::Type_Boolean,
76 "Remove the private tags from the DICOM instances (defaults to `false`)", false) 94 "Remove the private tags from the DICOM instances (defaults to `false`)", false)
77 .SetRequestField("Replace", RestApiCallDocumentation::Type_JsonObject, 95 .SetRequestField(REPLACE, RestApiCallDocumentation::Type_JsonObject,
78 "Associative array to change the value of some DICOM tags in the DICOM instances. " INFO_SUBSEQUENCES, false) 96 "Associative array to change the value of some DICOM tags in the DICOM instances. " INFO_SUBSEQUENCES, false)
79 .SetRequestField("Remove", RestApiCallDocumentation::Type_JsonListOfStrings, 97 .SetRequestField(REMOVE, RestApiCallDocumentation::Type_JsonListOfStrings,
80 "List of tags that must be removed from the DICOM instances. " INFO_SUBSEQUENCES, false) 98 "List of tags that must be removed from the DICOM instances. " INFO_SUBSEQUENCES, false)
81 .SetRequestField("Keep", RestApiCallDocumentation::Type_JsonListOfStrings, 99 .SetRequestField(KEEP, RestApiCallDocumentation::Type_JsonListOfStrings,
82 "Keep the original value of the specified tags, to be chosen among the `StudyInstanceUID`, " 100 "Keep the original value of the specified tags, to be chosen among the `StudyInstanceUID`, "
83 "`SeriesInstanceUID` and `SOPInstanceUID` tags. Avoid this feature as much as possible, " 101 "`SeriesInstanceUID` and `SOPInstanceUID` tags. Avoid this feature as much as possible, "
84 "as this breaks the DICOM model of the real world.", false) 102 "as this breaks the DICOM model of the real world.", false)
85 .SetRequestField("PrivateCreator", RestApiCallDocumentation::Type_String, 103 .SetRequestField(PRIVATE_CREATOR, RestApiCallDocumentation::Type_String,
86 "The private creator to be used for private tags in `Replace`", false); 104 "The private creator to be used for private tags in `Replace`", false);
87 } 105 }
88 106
89 107
90 static void DocumentAnonymizationOptions(RestApiPostCall& call) 108 static void DocumentAnonymizationOptions(RestApiPostCall& call)
91 { 109 {
92 // Check out "DicomModification::ParseAnonymizationRequest()" 110 // Check out "DicomModification::ParseAnonymizationRequest()"
93 call.GetDocumentation() 111 call.GetDocumentation()
94 .SetRequestField("Force", RestApiCallDocumentation::Type_Boolean, 112 .SetRequestField(FORCE, RestApiCallDocumentation::Type_Boolean,
95 "Allow the modification of tags related to DICOM identifiers, at the risk of " 113 "Allow the modification of tags related to DICOM identifiers, at the risk of "
96 "breaking the DICOM model of the real world", false) 114 "breaking the DICOM model of the real world", false)
97 .SetRequestField("DicomVersion", RestApiCallDocumentation::Type_String, 115 .SetRequestField("DicomVersion", RestApiCallDocumentation::Type_String,
98 "Version of the DICOM standard to be used for anonymization. Check out " 116 "Version of the DICOM standard to be used for anonymization. Check out "
99 "configuration option `DeidentifyLogsDicomVersion` for possible values.", false) 117 "configuration option `DeidentifyLogsDicomVersion` for possible values.", false)
100 .SetRequestField("KeepPrivateTags", RestApiCallDocumentation::Type_Boolean, 118 .SetRequestField(KEEP_PRIVATE_TAGS, RestApiCallDocumentation::Type_Boolean,
101 "Keep the private tags from the DICOM instances (defaults to `false`)", false) 119 "Keep the private tags from the DICOM instances (defaults to `false`)", false)
102 .SetRequestField("Replace", RestApiCallDocumentation::Type_JsonObject, 120 .SetRequestField(REPLACE, RestApiCallDocumentation::Type_JsonObject,
103 "Associative array to change the value of some DICOM tags in the DICOM instances. " INFO_SUBSEQUENCES, false) 121 "Associative array to change the value of some DICOM tags in the DICOM instances. " INFO_SUBSEQUENCES, false)
104 .SetRequestField("Remove", RestApiCallDocumentation::Type_JsonListOfStrings, 122 .SetRequestField(REMOVE, RestApiCallDocumentation::Type_JsonListOfStrings,
105 "List of additional tags to be removed from the DICOM instances. " INFO_SUBSEQUENCES, false) 123 "List of additional tags to be removed from the DICOM instances. " INFO_SUBSEQUENCES, false)
106 .SetRequestField("Keep", RestApiCallDocumentation::Type_JsonListOfStrings, 124 .SetRequestField(KEEP, RestApiCallDocumentation::Type_JsonListOfStrings,
107 "List of DICOM tags whose value must not be destroyed by the anonymization. " INFO_SUBSEQUENCES, false) 125 "List of DICOM tags whose value must not be destroyed by the anonymization. " INFO_SUBSEQUENCES, false)
108 .SetRequestField("PrivateCreator", RestApiCallDocumentation::Type_String, 126 .SetRequestField(PRIVATE_CREATOR, RestApiCallDocumentation::Type_String,
109 "The private creator to be used for private tags in `Replace`", false); 127 "The private creator to be used for private tags in `Replace`", false);
110 } 128 }
111 129
112 130
113 static void ParseModifyRequest(Json::Value& request, 131 static void ParseModifyRequest(Json::Value& request,
251 Json::Value request; 269 Json::Value request;
252 ParseModifyRequest(request, modification, call); 270 ParseModifyRequest(request, modification, call);
253 271
254 modification.SetLevel(DetectModifyLevel(modification)); 272 modification.SetLevel(DetectModifyLevel(modification));
255 273
256 static const char* TRANSCODE = "Transcode";
257 if (request.isMember(TRANSCODE)) 274 if (request.isMember(TRANSCODE))
258 { 275 {
259 std::string s = SerializationToolbox::ReadString(request, TRANSCODE); 276 std::string s = SerializationToolbox::ReadString(request, TRANSCODE);
260 277
261 DicomTransferSyntax syntax; 278 DicomTransferSyntax syntax;
303 320
304 321
305 static void SetKeepSource(CleaningInstancesJob& job, 322 static void SetKeepSource(CleaningInstancesJob& job,
306 const Json::Value& body) 323 const Json::Value& body)
307 { 324 {
308 static const char* KEEP_SOURCE = "KeepSource";
309 if (body.isMember(KEEP_SOURCE)) 325 if (body.isMember(KEEP_SOURCE))
310 { 326 {
311 job.SetKeepSource(SerializationToolbox::ReadBoolean(body, KEEP_SOURCE)); 327 job.SetKeepSource(SerializationToolbox::ReadBoolean(body, KEEP_SOURCE));
312 } 328 }
313 } 329 }
335 } 351 }
336 352
337 job->SetOrigin(call); 353 job->SetOrigin(call);
338 SetKeepSource(*job, body); 354 SetKeepSource(*job, body);
339 355
340 static const char* TRANSCODE = "Transcode";
341 if (body.isMember(TRANSCODE)) 356 if (body.isMember(TRANSCODE))
342 { 357 {
343 job->SetTranscode(SerializationToolbox::ReadString(body, TRANSCODE)); 358 job->SetTranscode(SerializationToolbox::ReadString(body, TRANSCODE));
344 } 359 }
345 360
375 bool isAnonymization, 390 bool isAnonymization,
376 RestApiPostCall& call, 391 RestApiPostCall& call,
377 const Json::Value& body) 392 const Json::Value& body)
378 { 393 {
379 std::set<std::string> resources; 394 std::set<std::string> resources;
380 SerializationToolbox::ReadSetOfStrings(resources, body, "Resources"); 395 SerializationToolbox::ReadSetOfStrings(resources, body, RESOURCES);
381 396
382 SubmitModificationJob(modification, isAnonymization, 397 SubmitModificationJob(modification, isAnonymization,
383 call, body, ResourceType_Instance /* arbitrary value, unused */, 398 call, body, ResourceType_Instance /* arbitrary value, unused */,
384 false /* multiple resources */, resources); 399 false /* multiple resources */, resources);
385 } 400 }
424 OrthancRestApi::DocumentSubmitCommandsJob(call); 439 OrthancRestApi::DocumentSubmitCommandsJob(call);
425 DocumentModifyOptions(call); 440 DocumentModifyOptions(call);
426 call.GetDocumentation() 441 call.GetDocumentation()
427 .SetTag("System") 442 .SetTag("System")
428 .SetSummary("Modify a set of resources") 443 .SetSummary("Modify a set of resources")
429 .SetRequestField("Resources", RestApiCallDocumentation::Type_JsonListOfStrings, 444 .SetRequestField(RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings,
430 "List of the Orthanc identifiers of the patients/studies/series/instances of interest.", true) 445 "List of the Orthanc identifiers of the patients/studies/series/instances of interest.", true)
431 .SetDescription("Start a job that will modify all the DICOM patients, studies, series or instances " 446 .SetDescription("Start a job that will modify all the DICOM patients, studies, series or instances "
432 "whose identifiers are provided in the `Resources` field.") 447 "whose identifiers are provided in the `Resources` field.")
433 .AddAnswerType(MimeType_Json, "The list of all the resources that have been altered by this modification"); 448 .AddAnswerType(MimeType_Json, "The list of all the resources that have been altered by this modification");
434 return; 449 return;
482 OrthancRestApi::DocumentSubmitCommandsJob(call); 497 OrthancRestApi::DocumentSubmitCommandsJob(call);
483 DocumentAnonymizationOptions(call); 498 DocumentAnonymizationOptions(call);
484 call.GetDocumentation() 499 call.GetDocumentation()
485 .SetTag("System") 500 .SetTag("System")
486 .SetSummary("Anonymize a set of resources") 501 .SetSummary("Anonymize a set of resources")
487 .SetRequestField("Resources", RestApiCallDocumentation::Type_JsonListOfStrings, 502 .SetRequestField(RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings,
488 "List of the Orthanc identifiers of the patients/studies/series/instances of interest.", true) 503 "List of the Orthanc identifiers of the patients/studies/series/instances of interest.", true)
489 .SetDescription("Start a job that will anonymize all the DICOM patients, studies, series or instances " 504 .SetDescription("Start a job that will anonymize all the DICOM patients, studies, series or instances "
490 "whose identifiers are provided in the `Resources` field.") 505 "whose identifiers are provided in the `Resources` field.")
491 .AddAnswerType(MimeType_Json, "The list of all the resources that have been created by this anonymization"); 506 .AddAnswerType(MimeType_Json, "The list of all the resources that have been created by this anonymization");
492 return; 507 return;
636 { 651 {
637 payload = &content[i]; 652 payload = &content[i];
638 } 653 }
639 else if (content[i].type() == Json::objectValue) 654 else if (content[i].type() == Json::objectValue)
640 { 655 {
641 if (!content[i].isMember("Content")) 656 if (!content[i].isMember(CONTENT))
642 { 657 {
643 throw OrthancException(ErrorCode_CreateDicomNoPayload); 658 throw OrthancException(ErrorCode_CreateDicomNoPayload);
644 } 659 }
645 660
646 payload = &content[i]["Content"]; 661 payload = &content[i][CONTENT];
647 662
648 if (content[i].isMember("Tags")) 663 if (content[i].isMember(TAGS))
649 { 664 {
650 InjectTags(*dicom, content[i]["Tags"], decodeBinaryTags, privateCreator, force); 665 InjectTags(*dicom, content[i][TAGS], decodeBinaryTags, privateCreator, force);
651 } 666 }
652 } 667 }
653 668
654 if (payload == NULL || 669 if (payload == NULL ||
655 payload->type() != Json::stringValue) 670 payload->type() != Json::stringValue)
687 702
688 703
689 static void CreateDicomV2(RestApiPostCall& call, 704 static void CreateDicomV2(RestApiPostCall& call,
690 const Json::Value& request) 705 const Json::Value& request)
691 { 706 {
692 static const char* const CONTENT = "Content";
693 static const char* const FORCE = "Force";
694 static const char* const INTERPRET_BINARY_TAGS = "InterpretBinaryTags";
695 static const char* const PARENT = "Parent";
696 static const char* const PRIVATE_CREATOR = "PrivateCreator";
697 static const char* const SPECIFIC_CHARACTER_SET_2 = "SpecificCharacterSet"; 707 static const char* const SPECIFIC_CHARACTER_SET_2 = "SpecificCharacterSet";
698 static const char* const TAGS = "Tags";
699 static const char* const TYPE = "Type"; 708 static const char* const TYPE = "Type";
700 static const char* const VALUE = "Value"; 709 static const char* const VALUE = "Value";
701 710
702 assert(request.isObject()); 711 assert(request.isObject());
703 ServerContext& context = OrthancRestApi::GetContext(call); 712 ServerContext& context = OrthancRestApi::GetContext(call);
767 } 776 }
768 777
769 778
770 // Choose the same encoding as the parent resource 779 // Choose the same encoding as the parent resource
771 { 780 {
772 static const char* SPECIFIC_CHARACTER_SET = "0008,0005"; 781 static const char* const SPECIFIC_CHARACTER_SET = "0008,0005";
773 782
774 if (siblingTags.isMember(SPECIFIC_CHARACTER_SET)) 783 if (siblingTags.isMember(SPECIFIC_CHARACTER_SET))
775 { 784 {
776 Encoding encoding; 785 Encoding encoding;
777 786
946 { 955 {
947 call.GetDocumentation() 956 call.GetDocumentation()
948 .SetTag("System") 957 .SetTag("System")
949 .SetSummary("Create one DICOM instance") 958 .SetSummary("Create one DICOM instance")
950 .SetDescription("Create one DICOM instance, and store it into Orthanc") 959 .SetDescription("Create one DICOM instance, and store it into Orthanc")
951 .SetRequestField("Tags", RestApiCallDocumentation::Type_JsonObject, 960 .SetRequestField(TAGS, RestApiCallDocumentation::Type_JsonObject,
952 "Associative array containing the tags of the new instance to be created", true) 961 "Associative array containing the tags of the new instance to be created", true)
953 .SetRequestField("Content", RestApiCallDocumentation::Type_String, 962 .SetRequestField(CONTENT, RestApiCallDocumentation::Type_String,
954 "This field can be used to embed an image (pixel data) or a PDF inside the created DICOM instance. " 963 "This field can be used to embed an image (pixel data) or a PDF inside the created DICOM instance. "
955 "The PNG image, the JPEG image or the PDF file must be provided using their " 964 "The PNG image, the JPEG image or the PDF file must be provided using their "
956 "[data URI scheme encoding](https://en.wikipedia.org/wiki/Data_URI_scheme). " 965 "[data URI scheme encoding](https://en.wikipedia.org/wiki/Data_URI_scheme). "
957 "This field can possibly contain a JSON array, in which case a DICOM series is created " 966 "This field can possibly contain a JSON array, in which case a DICOM series is created "
958 "containing one DICOM instance for each item in the `Content` field.", false) 967 "containing one DICOM instance for each item in the `Content` field.", false)
959 .SetRequestField("Parent", RestApiCallDocumentation::Type_String, 968 .SetRequestField(PARENT, RestApiCallDocumentation::Type_String,
960 "If present, the newly created instance will be attached to the parent DICOM resource " 969 "If present, the newly created instance will be attached to the parent DICOM resource "
961 "whose Orthanc identifier is contained in this field. The DICOM tags of the parent " 970 "whose Orthanc identifier is contained in this field. The DICOM tags of the parent "
962 "modules in the DICOM hierarchy will be automatically copied to the newly created instance.", false) 971 "modules in the DICOM hierarchy will be automatically copied to the newly created instance.", false)
963 .SetRequestField("InterpretBinaryTags", RestApiCallDocumentation::Type_Boolean, 972 .SetRequestField(INTERPRET_BINARY_TAGS, RestApiCallDocumentation::Type_Boolean,
964 "If some value in the `Tags` associative array is formatted according to some " 973 "If some value in the `Tags` associative array is formatted according to some "
965 "[data URI scheme encoding](https://en.wikipedia.org/wiki/Data_URI_scheme), " 974 "[data URI scheme encoding](https://en.wikipedia.org/wiki/Data_URI_scheme), "
966 "whether this value is decoded to a binary value or kept as such (`true` by default)", false) 975 "whether this value is decoded to a binary value or kept as such (`true` by default)", false)
967 .SetRequestField("PrivateCreator", RestApiCallDocumentation::Type_String, 976 .SetRequestField(PRIVATE_CREATOR, RestApiCallDocumentation::Type_String,
968 "The private creator to be used for private tags in `Tags`", false) 977 "The private creator to be used for private tags in `Tags`", false)
969 .SetRequestField("Force", RestApiCallDocumentation::Type_Boolean, 978 .SetRequestField(FORCE, RestApiCallDocumentation::Type_Boolean,
970 "Avoid the consistency checks for the DICOM tags that enforce the DICOM model of the real-world. " 979 "Avoid the consistency checks for the DICOM tags that enforce the DICOM model of the real-world. "
971 "You can notably use this flag if you need to manually set the tags `StudyInstanceUID`, " 980 "You can notably use this flag if you need to manually set the tags `StudyInstanceUID`, "
972 "`SeriesInstanceUID`, or `SOPInstanceUID`. Be careful with this feature.", false) 981 "`SeriesInstanceUID`, or `SOPInstanceUID`. Be careful with this feature.", false)
973 .SetAnswerField("ID", RestApiCallDocumentation::Type_String, "Orthanc identifier of the newly created instance") 982 .SetAnswerField("ID", RestApiCallDocumentation::Type_String, "Orthanc identifier of the newly created instance")
974 .SetAnswerField("Path", RestApiCallDocumentation::Type_String, "Path to access the instance in the REST API"); 983 .SetAnswerField("Path", RestApiCallDocumentation::Type_String, "Path to access the instance in the REST API");
980 !request.isObject()) 989 !request.isObject())
981 { 990 {
982 throw OrthancException(ErrorCode_BadRequest); 991 throw OrthancException(ErrorCode_BadRequest);
983 } 992 }
984 993
985 if (request.isMember("Tags")) 994 if (request.isMember(TAGS))
986 { 995 {
987 CreateDicomV2(call, request); 996 CreateDicomV2(call, request);
988 } 997 }
989 else 998 else
990 { 999 {
1005 OrthancRestApi::DocumentSubmitCommandsJob(call); 1014 OrthancRestApi::DocumentSubmitCommandsJob(call);
1006 call.GetDocumentation() 1015 call.GetDocumentation()
1007 .SetTag("Studies") 1016 .SetTag("Studies")
1008 .SetSummary("Split study") 1017 .SetSummary("Split study")
1009 .SetDescription("Start a new job so as to split the DICOM study whose Orthanc identifier is provided in the URL, " 1018 .SetDescription("Start a new job so as to split the DICOM study whose Orthanc identifier is provided in the URL, "
1010 "by taking some of its children series out of it and putting them into a brand new study (this " 1019 "by taking some of its children series or instances out of it and putting them into a brand new study "
1011 "new study is created by setting the `StudyInstanceUID` tag to a random identifier): " 1020 "(this new study is created by setting the `StudyInstanceUID` tag to a random identifier): "
1012 "https://book.orthanc-server.com/users/anonymization.html#splitting") 1021 "https://book.orthanc-server.com/users/anonymization.html#splitting")
1013 .SetUriArgument("id", "Orthanc identifier of the study of interest") 1022 .SetUriArgument("id", "Orthanc identifier of the study of interest")
1014 .SetRequestField("Series", RestApiCallDocumentation::Type_JsonListOfStrings, 1023 .SetRequestField(SERIES, RestApiCallDocumentation::Type_JsonListOfStrings,
1015 "The list of series to be separated from the parent study (mandatory option). " 1024 "The list of series to be separated from the parent study. "
1016 "These series must all be children of the same source study, that is specified in the URI.", true) 1025 "These series must all be children of the same source study, that is specified in the URI.", false)
1017 .SetRequestField("Replace", RestApiCallDocumentation::Type_JsonObject, 1026 .SetRequestField(REPLACE, RestApiCallDocumentation::Type_JsonObject,
1018 "Associative array to change the value of some DICOM tags in the new study. " 1027 "Associative array to change the value of some DICOM tags in the new study. "
1019 "These tags must be part of the \"Patient Module Attributes\" or the \"General Study " 1028 "These tags must be part of the \"Patient Module Attributes\" or the \"General Study "
1020 "Module Attributes\", as specified by the DICOM 2011 standard in Tables C.7-1 and C.7-3.", false) 1029 "Module Attributes\", as specified by the DICOM 2011 standard in Tables C.7-1 and C.7-3.", false)
1021 .SetRequestField("Remove", RestApiCallDocumentation::Type_JsonListOfStrings, 1030 .SetRequestField(REMOVE, RestApiCallDocumentation::Type_JsonListOfStrings,
1022 "List of tags that must be removed in the new study (from the same modules as in the `Replace` option)", false) 1031 "List of tags that must be removed in the new study (from the same modules as in the `Replace` option)", false)
1023 .SetRequestField("KeepSource", RestApiCallDocumentation::Type_Boolean, 1032 .SetRequestField(KEEP_SOURCE, RestApiCallDocumentation::Type_Boolean,
1024 "If set to `true`, instructs Orthanc to keep a copy of the original series in the source study. " 1033 "If set to `true`, instructs Orthanc to keep a copy of the original series/instances in the source study. "
1025 "By default, the original series are deleted from Orthanc.", false); 1034 "By default, the original series/instances are deleted from Orthanc.", false)
1035 .SetRequestField(INSTANCES, RestApiCallDocumentation::Type_JsonListOfStrings,
1036 "The list of instances to be separated from the parent study. "
1037 "These instances must all be children of the same source study, that is specified in the URI.", false);
1026 return; 1038 return;
1027 } 1039 }
1028 1040
1029 ServerContext& context = OrthancRestApi::GetContext(call); 1041 ServerContext& context = OrthancRestApi::GetContext(call);
1030 1042
1038 const std::string study = call.GetUriComponent("id", ""); 1050 const std::string study = call.GetUriComponent("id", "");
1039 1051
1040 std::unique_ptr<SplitStudyJob> job(new SplitStudyJob(context, study)); 1052 std::unique_ptr<SplitStudyJob> job(new SplitStudyJob(context, study));
1041 job->SetOrigin(call); 1053 job->SetOrigin(call);
1042 1054
1043 std::vector<std::string> series; 1055 bool ok = false;
1044 SerializationToolbox::ReadArrayOfStrings(series, request, "Series"); 1056 if (request.isMember(SERIES))
1045 1057 {
1046 for (size_t i = 0; i < series.size(); i++) 1058 std::vector<std::string> series;
1047 { 1059 SerializationToolbox::ReadArrayOfStrings(series, request, SERIES);
1048 job->AddSourceSeries(series[i]); 1060
1049 } 1061 for (size_t i = 0; i < series.size(); i++)
1062 {
1063 job->AddSourceSeries(series[i]);
1064 ok = true;
1065 }
1066 }
1067
1068 if (request.isMember(INSTANCES))
1069 {
1070 std::vector<std::string> instances;
1071 SerializationToolbox::ReadArrayOfStrings(instances, request, INSTANCES);
1072
1073 for (size_t i = 0; i < instances.size(); i++)
1074 {
1075 job->AddSourceInstance(instances[i]);
1076 ok = true;
1077 }
1078 }
1079
1080 if (!ok)
1081 {
1082 throw OrthancException(ErrorCode_BadRequest, "Both the \"Series\" and the \"Instances\" fields are missing");
1083 }
1050 1084
1051 job->AddTrailingStep(); 1085 job->AddTrailingStep();
1052 1086
1053 SetKeepSource(*job, request); 1087 SetKeepSource(*job, request);
1054 1088
1055 static const char* REMOVE = "Remove";
1056 if (request.isMember(REMOVE)) 1089 if (request.isMember(REMOVE))
1057 { 1090 {
1058 if (request[REMOVE].type() != Json::arrayValue) 1091 if (request[REMOVE].type() != Json::arrayValue)
1059 { 1092 {
1060 throw OrthancException(ErrorCode_BadFileFormat); 1093 throw OrthancException(ErrorCode_BadFileFormat);
1071 job->Remove(FromDcmtkBridge::ParseTag(request[REMOVE][i].asCString())); 1104 job->Remove(FromDcmtkBridge::ParseTag(request[REMOVE][i].asCString()));
1072 } 1105 }
1073 } 1106 }
1074 } 1107 }
1075 1108
1076 static const char* REPLACE = "Replace";
1077 if (request.isMember(REPLACE)) 1109 if (request.isMember(REPLACE))
1078 { 1110 {
1079 if (request[REPLACE].type() != Json::objectValue) 1111 if (request[REPLACE].type() != Json::objectValue)
1080 { 1112 {
1081 throw OrthancException(ErrorCode_BadFileFormat); 1113 throw OrthancException(ErrorCode_BadFileFormat);
1112 .SetTag("Studies") 1144 .SetTag("Studies")
1113 .SetSummary("Merge study") 1145 .SetSummary("Merge study")
1114 .SetDescription("Start a new job so as to move some DICOM series into the DICOM study whose Orthanc identifier " 1146 .SetDescription("Start a new job so as to move some DICOM series into the DICOM study whose Orthanc identifier "
1115 "is provided in the URL: https://book.orthanc-server.com/users/anonymization.html#merging") 1147 "is provided in the URL: https://book.orthanc-server.com/users/anonymization.html#merging")
1116 .SetUriArgument("id", "Orthanc identifier of the study of interest") 1148 .SetUriArgument("id", "Orthanc identifier of the study of interest")
1117 .SetRequestField("Resources", RestApiCallDocumentation::Type_JsonListOfStrings, 1149 .SetRequestField(RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings,
1118 "The list of DICOM resources (patients, studies, series, and/or instances) to be merged " 1150 "The list of DICOM resources (patients, studies, series, and/or instances) to be merged "
1119 "into the study of interest (mandatory option)", true) 1151 "into the study of interest (mandatory option)", true)
1120 .SetRequestField("KeepSource", RestApiCallDocumentation::Type_Boolean, 1152 .SetRequestField(KEEP_SOURCE, RestApiCallDocumentation::Type_Boolean,
1121 "If set to `true`, instructs Orthanc to keep a copy of the original resources in their source study. " 1153 "If set to `true`, instructs Orthanc to keep a copy of the original resources in their source study. "
1122 "By default, the original resources are deleted from Orthanc.", false); 1154 "By default, the original resources are deleted from Orthanc.", false);
1123 return; 1155 return;
1124 } 1156 }
1125 1157
1136 1168
1137 std::unique_ptr<MergeStudyJob> job(new MergeStudyJob(context, study)); 1169 std::unique_ptr<MergeStudyJob> job(new MergeStudyJob(context, study));
1138 job->SetOrigin(call); 1170 job->SetOrigin(call);
1139 1171
1140 std::vector<std::string> resources; 1172 std::vector<std::string> resources;
1141 SerializationToolbox::ReadArrayOfStrings(resources, request, "Resources"); 1173 SerializationToolbox::ReadArrayOfStrings(resources, request, RESOURCES);
1142 1174
1143 for (size_t i = 0; i < resources.size(); i++) 1175 for (size_t i = 0; i < resources.size(); i++)
1144 { 1176 {
1145 job->AddSource(resources[i]); 1177 job->AddSource(resources[i]);
1146 } 1178 }