1 /** 2 * Orthanc - A Lightweight, RESTful DICOM Store 3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics 4 * Department, University Hospital of Liege, Belgium 5 * Copyright (C) 2017-2021 Osimis S.A., Belgium 6 * 7 * This program is free software: you can redistribute it and/or 8 * modify it under the terms of the GNU General Public License as 9 * published by the Free Software Foundation, either version 3 of the 10 * License, or (at your option) any later version. 11 * 12 * In addition, as a special exception, the copyright holders of this 13 * program give permission to link the code of its release with the 14 * OpenSSL project's "OpenSSL" library (or with modified versions of it 15 * that use the same license as the "OpenSSL" library), and distribute 16 * the linked executables. You must obey the GNU General Public License 17 * in all respects for all of the code used other than "OpenSSL". If you 18 * modify file(s) with this exception, you may extend this exception to 19 * your version of the file(s), but you are not obligated to do so. If 20 * you do not wish to do so, delete this exception statement from your 21 * version. If you delete this exception statement from all source files 22 * in the program, then also delete it here. 23 * 24 * This program is distributed in the hope that it will be useful, but 25 * WITHOUT ANY WARRANTY; without even the implied warranty of 26 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 27 * General Public License for more details. 28 * 29 * You should have received a copy of the GNU General Public License 30 * along with this program. If not, see <http://www.gnu.org/licenses/>. 31 **/ 32 33 34 #include "../PrecompiledHeadersServer.h" 35 #include "OrthancRestApi.h" 36 37 #include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" 38 #include "../../../OrthancFramework/Sources/Logging.h" 39 #include "../../../OrthancFramework/Sources/SerializationToolbox.h" 40 #include "../OrthancConfiguration.h" 41 #include "../ServerContext.h" 42 #include "../ServerJobs/MergeStudyJob.h" 43 #include "../ServerJobs/ResourceModificationJob.h" 44 #include "../ServerJobs/SplitStudyJob.h" 45 46 #include <boost/lexical_cast.hpp> 47 #include <boost/algorithm/string/predicate.hpp> 48 49 #define INFO_SUBSEQUENCES \ 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)." 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 LEVEL = "Level"; 62 static const char* const PARENT = "Parent"; 63 static const char* const PRIVATE_CREATOR = "PrivateCreator"; 64 static const char* const REMOVE = "Remove"; 65 static const char* const REPLACE = "Replace"; 66 static const char* const RESOURCES = "Resources"; 67 static const char* const SERIES = "Series"; 68 static const char* const TAGS = "Tags"; 69 static const char* const TRANSCODE = "Transcode"; 70 71 72 namespace Orthanc 73 { 74 // Modification of DICOM instances ------------------------------------------ 75 76 GeneratePatientName(ServerContext & context)77 static std::string GeneratePatientName(ServerContext& context) 78 { 79 uint64_t seq = context.GetIndex().IncrementGlobalSequence(GlobalProperty_AnonymizationSequence, true /* shared */); 80 return "Anonymized" + boost::lexical_cast<std::string>(seq); 81 } 82 83 DocumentKeepSource(RestApiPostCall & call)84 static void DocumentKeepSource(RestApiPostCall& call) 85 { 86 call.GetDocumentation() 87 .SetRequestField(KEEP_SOURCE, RestApiCallDocumentation::Type_Boolean, 88 "If set to `false`, instructs Orthanc to the remove original resources. " 89 "By default, the original resources are kept in Orthanc.", false); 90 } 91 92 DocumentModifyOptions(RestApiPostCall & call)93 static void DocumentModifyOptions(RestApiPostCall& call) 94 { 95 // Check out "DicomModification::ParseModifyRequest()" 96 call.GetDocumentation() 97 .SetRequestField(TRANSCODE, RestApiCallDocumentation::Type_String, 98 "Transcode the DICOM instances to the provided DICOM transfer syntax: " 99 "https://book.orthanc-server.com/faq/transcoding.html", false) 100 .SetRequestField(FORCE, RestApiCallDocumentation::Type_Boolean, 101 "Allow the modification of tags related to DICOM identifiers, at the risk of " 102 "breaking the DICOM model of the real world", false) 103 .SetRequestField("RemovePrivateTags", RestApiCallDocumentation::Type_Boolean, 104 "Remove the private tags from the DICOM instances (defaults to `false`)", false) 105 .SetRequestField(REPLACE, RestApiCallDocumentation::Type_JsonObject, 106 "Associative array to change the value of some DICOM tags in the DICOM instances. " INFO_SUBSEQUENCES, false) 107 .SetRequestField(REMOVE, RestApiCallDocumentation::Type_JsonListOfStrings, 108 "List of tags that must be removed from the DICOM instances. " INFO_SUBSEQUENCES, false) 109 .SetRequestField(KEEP, RestApiCallDocumentation::Type_JsonListOfStrings, 110 "Keep the original value of the specified tags, to be chosen among the `StudyInstanceUID`, " 111 "`SeriesInstanceUID` and `SOPInstanceUID` tags. Avoid this feature as much as possible, " 112 "as this breaks the DICOM model of the real world.", false) 113 .SetRequestField(PRIVATE_CREATOR, RestApiCallDocumentation::Type_String, 114 "The private creator to be used for private tags in `Replace`", false); 115 116 // This was existing, but undocumented in Orthanc <= 1.9.6 117 DocumentKeepSource(call); 118 } 119 120 DocumentAnonymizationOptions(RestApiPostCall & call)121 static void DocumentAnonymizationOptions(RestApiPostCall& call) 122 { 123 // Check out "DicomModification::ParseAnonymizationRequest()" 124 call.GetDocumentation() 125 .SetRequestField(FORCE, RestApiCallDocumentation::Type_Boolean, 126 "Allow the modification of tags related to DICOM identifiers, at the risk of " 127 "breaking the DICOM model of the real world", false) 128 .SetRequestField("DicomVersion", RestApiCallDocumentation::Type_String, 129 "Version of the DICOM standard to be used for anonymization. Check out " 130 "configuration option `DeidentifyLogsDicomVersion` for possible values.", false) 131 .SetRequestField(KEEP_PRIVATE_TAGS, RestApiCallDocumentation::Type_Boolean, 132 "Keep the private tags from the DICOM instances (defaults to `false`)", false) 133 .SetRequestField(REPLACE, RestApiCallDocumentation::Type_JsonObject, 134 "Associative array to change the value of some DICOM tags in the DICOM instances. " INFO_SUBSEQUENCES, false) 135 .SetRequestField(REMOVE, RestApiCallDocumentation::Type_JsonListOfStrings, 136 "List of additional tags to be removed from the DICOM instances. " INFO_SUBSEQUENCES, false) 137 .SetRequestField(KEEP, RestApiCallDocumentation::Type_JsonListOfStrings, 138 "List of DICOM tags whose value must not be destroyed by the anonymization. " INFO_SUBSEQUENCES, false) 139 .SetRequestField(PRIVATE_CREATOR, RestApiCallDocumentation::Type_String, 140 "The private creator to be used for private tags in `Replace`", false); 141 142 // This was existing, but undocumented in Orthanc <= 1.9.6 143 DocumentKeepSource(call); 144 } 145 146 ParseModifyRequest(Json::Value & request,DicomModification & target,const RestApiPostCall & call)147 static void ParseModifyRequest(Json::Value& request, 148 DicomModification& target, 149 const RestApiPostCall& call) 150 { 151 // curl http://localhost:8042/series/95a6e2bf-9296e2cc-bf614e2f-22b391ee-16e010e0/modify -X POST -d '{"Replace":{"InstitutionName":"My own clinic"},"Priority":9}' 152 153 { 154 OrthancConfiguration::ReaderLock lock; 155 target.SetPrivateCreator(lock.GetConfiguration().GetDefaultPrivateCreator()); 156 } 157 158 if (call.ParseJsonRequest(request)) 159 { 160 target.ParseModifyRequest(request); 161 } 162 else 163 { 164 throw OrthancException(ErrorCode_BadFileFormat); 165 } 166 } 167 168 ParseAnonymizationRequest(Json::Value & request,DicomModification & target,RestApiPostCall & call)169 static void ParseAnonymizationRequest(Json::Value& request, 170 DicomModification& target, 171 RestApiPostCall& call) 172 { 173 // curl http://localhost:8042/instances/6e67da51-d119d6ae-c5667437-87b9a8a5-0f07c49f/anonymize -X POST -d '{"Replace":{"PatientName":"hello","0010-0020":"world"},"Keep":["StudyDescription", "SeriesDescription"],"KeepPrivateTags": true,"Remove":["Modality"]}' > Anonymized.dcm 174 175 { 176 OrthancConfiguration::ReaderLock lock; 177 target.SetPrivateCreator(lock.GetConfiguration().GetDefaultPrivateCreator()); 178 } 179 180 if (call.ParseJsonRequest(request) && 181 request.isObject()) 182 { 183 bool patientNameOverridden; 184 target.ParseAnonymizationRequest(patientNameOverridden, request); 185 186 if (!patientNameOverridden) 187 { 188 // Override the random Patient's Name by one that is more 189 // user-friendly (provided none was specified by the user) 190 target.Replace(DICOM_TAG_PATIENT_NAME, GeneratePatientName(OrthancRestApi::GetContext(call)), true); 191 } 192 } 193 else 194 { 195 throw OrthancException(ErrorCode_BadFileFormat); 196 } 197 } 198 199 AnonymizeOrModifyInstance(DicomModification & modification,RestApiPostCall & call,bool transcode,DicomTransferSyntax targetSyntax)200 static void AnonymizeOrModifyInstance(DicomModification& modification, 201 RestApiPostCall& call, 202 bool transcode, 203 DicomTransferSyntax targetSyntax) 204 { 205 ServerContext& context = OrthancRestApi::GetContext(call); 206 std::string id = call.GetUriComponent("id", ""); 207 208 std::unique_ptr<ParsedDicomFile> modified; 209 210 { 211 ServerContext::DicomCacheLocker locker(context, id); 212 modified.reset(locker.GetDicom().Clone(true)); 213 } 214 215 modification.Apply(*modified); 216 217 if (transcode) 218 { 219 IDicomTranscoder::DicomImage source; 220 source.AcquireParsed(*modified); // "modified" is invalid below this point 221 222 IDicomTranscoder::DicomImage transcoded; 223 224 std::set<DicomTransferSyntax> s; 225 s.insert(targetSyntax); 226 227 if (context.Transcode(transcoded, source, s, true)) 228 { 229 call.GetOutput().AnswerBuffer(transcoded.GetBufferData(), 230 transcoded.GetBufferSize(), MimeType_Dicom); 231 } 232 else 233 { 234 throw OrthancException(ErrorCode_InternalError, 235 "Cannot transcode to transfer syntax: " + 236 std::string(GetTransferSyntaxUid(targetSyntax))); 237 } 238 } 239 else 240 { 241 modified->Answer(call.GetOutput()); 242 } 243 } 244 245 DetectModifyLevel(const DicomModification & modification)246 static ResourceType DetectModifyLevel(const DicomModification& modification) 247 { 248 if (modification.IsReplaced(DICOM_TAG_PATIENT_ID)) 249 { 250 return ResourceType_Patient; 251 } 252 else if (modification.IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) 253 { 254 return ResourceType_Study; 255 } 256 else if (modification.IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) 257 { 258 return ResourceType_Series; 259 } 260 else 261 { 262 return ResourceType_Instance; 263 } 264 } 265 266 ModifyInstance(RestApiPostCall & call)267 static void ModifyInstance(RestApiPostCall& call) 268 { 269 if (call.IsDocumentation()) 270 { 271 DocumentModifyOptions(call); 272 call.GetDocumentation() 273 .SetTag("Instances") 274 .SetSummary("Modify instance") 275 .SetDescription("Download a modified version of the DICOM instance whose Orthanc identifier is provided in the URL: " 276 "https://book.orthanc-server.com/users/anonymization.html#modification-of-a-single-instance") 277 .SetUriArgument("id", "Orthanc identifier of the instance of interest") 278 .AddAnswerType(MimeType_Dicom, "The modified DICOM instance"); 279 return; 280 } 281 282 DicomModification modification; 283 modification.SetAllowManualIdentifiers(true); 284 285 Json::Value request; 286 ParseModifyRequest(request, modification, call); 287 288 modification.SetLevel(DetectModifyLevel(modification)); 289 290 if (request.isMember(TRANSCODE)) 291 { 292 std::string s = SerializationToolbox::ReadString(request, TRANSCODE); 293 294 DicomTransferSyntax syntax; 295 if (LookupTransferSyntax(syntax, s)) 296 { 297 AnonymizeOrModifyInstance(modification, call, true, syntax); 298 } 299 else 300 { 301 throw OrthancException(ErrorCode_ParameterOutOfRange, "Unknown transfer syntax: " + s); 302 } 303 } 304 else 305 { 306 AnonymizeOrModifyInstance(modification, call, false /* no transcoding */, 307 DicomTransferSyntax_LittleEndianImplicit /* unused */); 308 } 309 } 310 311 AnonymizeInstance(RestApiPostCall & call)312 static void AnonymizeInstance(RestApiPostCall& call) 313 { 314 if (call.IsDocumentation()) 315 { 316 DocumentAnonymizationOptions(call); 317 call.GetDocumentation() 318 .SetTag("Instances") 319 .SetSummary("Anonymize instance") 320 .SetDescription("Download an anonymized version of the DICOM instance whose Orthanc identifier is provided in the URL: " 321 "https://book.orthanc-server.com/users/anonymization.html#anonymization-of-a-single-instance") 322 .SetUriArgument("id", "Orthanc identifier of the instance of interest") 323 .AddAnswerType(MimeType_Dicom, "The anonymized DICOM instance"); 324 return; 325 } 326 327 DicomModification modification; 328 modification.SetAllowManualIdentifiers(true); 329 330 Json::Value request; 331 ParseAnonymizationRequest(request, modification, call); 332 333 AnonymizeOrModifyInstance(modification, call, false /* no transcoding */, 334 DicomTransferSyntax_LittleEndianImplicit /* unused */); 335 } 336 337 SetKeepSource(CleaningInstancesJob & job,const Json::Value & body)338 static void SetKeepSource(CleaningInstancesJob& job, 339 const Json::Value& body) 340 { 341 if (body.isMember(KEEP_SOURCE)) 342 { 343 job.SetKeepSource(SerializationToolbox::ReadBoolean(body, KEEP_SOURCE)); 344 } 345 } 346 347 SubmitModificationJob(std::unique_ptr<DicomModification> & modification,bool isAnonymization,RestApiPostCall & call,const Json::Value & body,ResourceType outputLevel,bool isSingleResource,const std::set<std::string> & resources)348 static void SubmitModificationJob(std::unique_ptr<DicomModification>& modification, 349 bool isAnonymization, 350 RestApiPostCall& call, 351 const Json::Value& body, 352 ResourceType outputLevel /* unused for multiple resources */, 353 bool isSingleResource, 354 const std::set<std::string>& resources) 355 { 356 ServerContext& context = OrthancRestApi::GetContext(call); 357 358 std::unique_ptr<ResourceModificationJob> job(new ResourceModificationJob(context)); 359 360 if (isSingleResource) // This notably configures the output format 361 { 362 job->SetSingleResourceModification(modification.release(), outputLevel, isAnonymization); 363 } 364 else 365 { 366 job->SetMultipleResourcesModification(modification.release(), isAnonymization); 367 } 368 369 job->SetOrigin(call); 370 SetKeepSource(*job, body); 371 372 if (body.isMember(TRANSCODE)) 373 { 374 job->SetTranscode(SerializationToolbox::ReadString(body, TRANSCODE)); 375 } 376 377 for (std::set<std::string>::const_iterator 378 it = resources.begin(); it != resources.end(); ++it) 379 { 380 context.AddChildInstances(*job, *it); 381 } 382 383 job->AddTrailingStep(); 384 385 OrthancRestApi::GetApi(call).SubmitCommandsJob 386 (call, job.release(), true /* synchronous by default */, body); 387 } 388 389 SubmitModificationJob(std::unique_ptr<DicomModification> & modification,bool isAnonymization,RestApiPostCall & call,const Json::Value & body,ResourceType outputLevel)390 static void SubmitModificationJob(std::unique_ptr<DicomModification>& modification, 391 bool isAnonymization, 392 RestApiPostCall& call, 393 const Json::Value& body, 394 ResourceType outputLevel) 395 { 396 // This was the only flavor in Orthanc <= 1.9.3 397 std::set<std::string> resources; 398 resources.insert(call.GetUriComponent("id", "")); 399 400 SubmitModificationJob(modification, isAnonymization, call, body, outputLevel, 401 true /* single resource */, resources); 402 } 403 404 SubmitBulkJob(std::unique_ptr<DicomModification> & modification,bool isAnonymization,RestApiPostCall & call,const Json::Value & body)405 static void SubmitBulkJob(std::unique_ptr<DicomModification>& modification, 406 bool isAnonymization, 407 RestApiPostCall& call, 408 const Json::Value& body) 409 { 410 std::set<std::string> resources; 411 SerializationToolbox::ReadSetOfStrings(resources, body, RESOURCES); 412 413 SubmitModificationJob(modification, isAnonymization, 414 call, body, ResourceType_Instance /* arbitrary value, unused */, 415 false /* multiple resources */, resources); 416 } 417 418 419 template <enum ResourceType resourceType> ModifyResource(RestApiPostCall & call)420 static void ModifyResource(RestApiPostCall& call) 421 { 422 if (call.IsDocumentation()) 423 { 424 OrthancRestApi::DocumentSubmitCommandsJob(call); 425 DocumentModifyOptions(call); 426 const std::string r = GetResourceTypeText(resourceType, false /* plural */, false /* lower case */); 427 call.GetDocumentation() 428 .SetTag(GetResourceTypeText(resourceType, true /* plural */, true /* upper case */)) 429 .SetSummary("Modify " + r) 430 .SetDescription("Start a job that will modify all the DICOM instances within the " + r + 431 " whose identifier is provided in the URL. The modified DICOM instances will be " 432 "stored into a brand new " + r + ", whose Orthanc identifiers will be returned by the job. " 433 "https://book.orthanc-server.com/users/anonymization.html#modification-of-studies-or-series") 434 .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest"); 435 return; 436 } 437 438 std::unique_ptr<DicomModification> modification(new DicomModification); 439 440 Json::Value body; 441 ParseModifyRequest(body, *modification, call); 442 443 modification->SetLevel(resourceType); 444 445 SubmitModificationJob(modification, false /* not an anonymization */, 446 call, body, resourceType); 447 } 448 449 450 // New in Orthanc 1.9.4 BulkModify(RestApiPostCall & call)451 static void BulkModify(RestApiPostCall& call) 452 { 453 if (call.IsDocumentation()) 454 { 455 OrthancRestApi::DocumentSubmitCommandsJob(call); 456 DocumentModifyOptions(call); 457 call.GetDocumentation() 458 .SetTag("System") 459 .SetSummary("Modify a set of resources") 460 .SetRequestField(RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings, 461 "List of the Orthanc identifiers of the patients/studies/series/instances of interest.", true) 462 .SetRequestField(LEVEL, RestApiCallDocumentation::Type_String, 463 "Level of the modification (`Patient`, `Study`, `Series` or `Instance`). If absent, " 464 "the level defaults to `Instance`, but is set to `Patient` if `PatientID` is modified, " 465 "to `Study` if `StudyInstanceUID` is modified, or to `Series` if `SeriesInstancesUID` " 466 "is modified. (new in Orthanc 1.9.7)", false) 467 .SetDescription("Start a job that will modify all the DICOM patients, studies, series or instances " 468 "whose identifiers are provided in the `Resources` field.") 469 .AddAnswerType(MimeType_Json, "The list of all the resources that have been altered by this modification"); 470 return; 471 } 472 473 std::unique_ptr<DicomModification> modification(new DicomModification); 474 475 Json::Value body; 476 ParseModifyRequest(body, *modification, call); 477 478 if (body.isMember(LEVEL)) 479 { 480 // This case was introduced in Orthanc 1.9.7 481 modification->SetLevel(StringToResourceType(body[LEVEL].asCString())); 482 } 483 else 484 { 485 modification->SetLevel(DetectModifyLevel(*modification)); 486 } 487 488 SubmitBulkJob(modification, false /* not an anonymization */, call, body); 489 } 490 491 492 template <enum ResourceType resourceType> AnonymizeResource(RestApiPostCall & call)493 static void AnonymizeResource(RestApiPostCall& call) 494 { 495 if (call.IsDocumentation()) 496 { 497 OrthancRestApi::DocumentSubmitCommandsJob(call); 498 DocumentAnonymizationOptions(call); 499 const std::string r = GetResourceTypeText(resourceType, false /* plural */, false /* lower case */); 500 call.GetDocumentation() 501 .SetTag(GetResourceTypeText(resourceType, true /* plural */, true /* upper case */)) 502 .SetSummary("Anonymize " + r) 503 .SetDescription("Start a job that will anonymize all the DICOM instances within the " + r + 504 " whose identifier is provided in the URL. The modified DICOM instances will be " 505 "stored into a brand new " + r + ", whose Orthanc identifiers will be returned by the job. " 506 "https://book.orthanc-server.com/users/anonymization.html#anonymization-of-patients-studies-or-series") 507 .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest"); 508 return; 509 } 510 511 std::unique_ptr<DicomModification> modification(new DicomModification); 512 513 Json::Value body; 514 ParseAnonymizationRequest(body, *modification, call); 515 516 SubmitModificationJob(modification, true /* anonymization */, 517 call, body, resourceType); 518 } 519 520 521 // New in Orthanc 1.9.4 BulkAnonymize(RestApiPostCall & call)522 static void BulkAnonymize(RestApiPostCall& call) 523 { 524 if (call.IsDocumentation()) 525 { 526 OrthancRestApi::DocumentSubmitCommandsJob(call); 527 DocumentAnonymizationOptions(call); 528 call.GetDocumentation() 529 .SetTag("System") 530 .SetSummary("Anonymize a set of resources") 531 .SetRequestField(RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings, 532 "List of the Orthanc identifiers of the patients/studies/series/instances of interest.", true) 533 .SetDescription("Start a job that will anonymize all the DICOM patients, studies, series or instances " 534 "whose identifiers are provided in the `Resources` field.") 535 .AddAnswerType(MimeType_Json, "The list of all the resources that have been created by this anonymization"); 536 return; 537 } 538 539 std::unique_ptr<DicomModification> modification(new DicomModification); 540 541 Json::Value body; 542 ParseAnonymizationRequest(body, *modification, call); 543 544 SubmitBulkJob(modification, true /* anonymization */, call, body); 545 } 546 547 StoreCreatedInstance(std::string & id,RestApiPostCall & call,ParsedDicomFile & dicom,bool sendAnswer)548 static void StoreCreatedInstance(std::string& id /* out */, 549 RestApiPostCall& call, 550 ParsedDicomFile& dicom, 551 bool sendAnswer) 552 { 553 std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(dicom)); 554 toStore->SetOrigin(DicomInstanceOrigin::FromRest(call)); 555 556 ServerContext& context = OrthancRestApi::GetContext(call); 557 StoreStatus status = context.Store(id, *toStore, StoreInstanceMode_Default); 558 559 if (status == StoreStatus_Failure) 560 { 561 throw OrthancException(ErrorCode_CannotStoreInstance); 562 } 563 564 if (sendAnswer) 565 { 566 OrthancRestApi::GetApi(call).AnswerStoredInstance(call, *toStore, status, id); 567 } 568 } 569 570 CreateDicomV1(ParsedDicomFile & dicom,RestApiPostCall & call,const Json::Value & request)571 static void CreateDicomV1(ParsedDicomFile& dicom, 572 RestApiPostCall& call, 573 const Json::Value& request) 574 { 575 // curl http://localhost:8042/tools/create-dicom -X POST -d '{"PatientName":"Hello^World"}' 576 // curl http://localhost:8042/tools/create-dicom -X POST -d '{"PatientName":"Hello^World","PixelData":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII="}' 577 578 assert(request.isObject()); 579 LOG(WARNING) << "Using a deprecated call to /tools/create-dicom"; 580 581 Json::Value::Members members = request.getMemberNames(); 582 for (size_t i = 0; i < members.size(); i++) 583 { 584 const std::string& name = members[i]; 585 if (request[name].type() != Json::stringValue) 586 { 587 throw OrthancException(ErrorCode_CreateDicomNotString); 588 } 589 590 std::string value = request[name].asString(); 591 592 DicomTag tag = FromDcmtkBridge::ParseTag(name); 593 if (tag == DICOM_TAG_PIXEL_DATA) 594 { 595 dicom.EmbedContent(value); 596 } 597 else 598 { 599 // This is V1, don't try and decode data URI scheme 600 dicom.ReplacePlainString(tag, value); 601 } 602 } 603 } 604 605 InjectTags(ParsedDicomFile & dicom,const Json::Value & tags,bool decodeBinaryTags,const std::string & privateCreator,bool force)606 static void InjectTags(ParsedDicomFile& dicom, 607 const Json::Value& tags, 608 bool decodeBinaryTags, 609 const std::string& privateCreator, 610 bool force) 611 { 612 if (tags.type() != Json::objectValue) 613 { 614 throw OrthancException(ErrorCode_BadRequest, "Tags field is not an array"); 615 } 616 617 // Inject the user-specified tags 618 Json::Value::Members members = tags.getMemberNames(); 619 for (size_t i = 0; i < members.size(); i++) 620 { 621 const std::string& name = members[i]; 622 DicomTag tag = FromDcmtkBridge::ParseTag(name); 623 624 if (tag != DICOM_TAG_SPECIFIC_CHARACTER_SET) 625 { 626 if (!force && 627 tag != DICOM_TAG_PATIENT_ID && 628 tag != DICOM_TAG_ACQUISITION_DATE && 629 tag != DICOM_TAG_ACQUISITION_TIME && 630 tag != DICOM_TAG_CONTENT_DATE && 631 tag != DICOM_TAG_CONTENT_TIME && 632 tag != DICOM_TAG_INSTANCE_CREATION_DATE && 633 tag != DICOM_TAG_INSTANCE_CREATION_TIME && 634 tag != DICOM_TAG_SERIES_DATE && 635 tag != DICOM_TAG_SERIES_TIME && 636 tag != DICOM_TAG_STUDY_DATE && 637 tag != DICOM_TAG_STUDY_TIME && 638 dicom.HasTag(tag)) 639 { 640 throw OrthancException(ErrorCode_CreateDicomOverrideTag, name); 641 } 642 643 if (tag == DICOM_TAG_PIXEL_DATA) 644 { 645 throw OrthancException(ErrorCode_CreateDicomUseContent); 646 } 647 else 648 { 649 dicom.Replace(tag, tags[name], decodeBinaryTags, DicomReplaceMode_InsertIfAbsent, privateCreator); 650 } 651 } 652 } 653 } 654 655 CreateSeries(RestApiPostCall & call,ParsedDicomFile & base,const Json::Value & content,bool decodeBinaryTags,const std::string & privateCreator,bool force)656 static void CreateSeries(RestApiPostCall& call, 657 ParsedDicomFile& base /* in */, 658 const Json::Value& content, 659 bool decodeBinaryTags, 660 const std::string& privateCreator, 661 bool force) 662 { 663 assert(content.isArray()); 664 assert(content.size() > 0); 665 ServerContext& context = OrthancRestApi::GetContext(call); 666 667 base.ReplacePlainString(DICOM_TAG_IMAGES_IN_ACQUISITION, boost::lexical_cast<std::string>(content.size())); 668 base.ReplacePlainString(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS, "1"); 669 670 std::string someInstance; 671 672 try 673 { 674 for (Json::ArrayIndex i = 0; i < content.size(); i++) 675 { 676 std::unique_ptr<ParsedDicomFile> dicom(base.Clone(false)); 677 const Json::Value* payload = NULL; 678 679 if (content[i].type() == Json::stringValue) 680 { 681 payload = &content[i]; 682 } 683 else if (content[i].type() == Json::objectValue) 684 { 685 if (!content[i].isMember(CONTENT)) 686 { 687 throw OrthancException(ErrorCode_CreateDicomNoPayload); 688 } 689 690 payload = &content[i][CONTENT]; 691 692 if (content[i].isMember(TAGS)) 693 { 694 InjectTags(*dicom, content[i][TAGS], decodeBinaryTags, privateCreator, force); 695 } 696 } 697 698 if (payload == NULL || 699 payload->type() != Json::stringValue) 700 { 701 throw OrthancException(ErrorCode_CreateDicomUseDataUriScheme); 702 } 703 704 dicom->EmbedContent(payload->asString()); 705 dicom->ReplacePlainString(DICOM_TAG_INSTANCE_NUMBER, boost::lexical_cast<std::string>(i + 1)); 706 dicom->ReplacePlainString(DICOM_TAG_IMAGE_INDEX, boost::lexical_cast<std::string>(i + 1)); 707 708 StoreCreatedInstance(someInstance, call, *dicom, false); 709 } 710 } 711 catch (OrthancException&) 712 { 713 // Error: Remove the newly-created series 714 715 std::string series; 716 if (context.GetIndex().LookupParent(series, someInstance)) 717 { 718 Json::Value dummy; 719 context.GetIndex().DeleteResource(dummy, series, ResourceType_Series); 720 } 721 722 throw; 723 } 724 725 std::string series; 726 if (context.GetIndex().LookupParent(series, someInstance)) 727 { 728 OrthancRestApi::GetApi(call).AnswerStoredResource(call, series, ResourceType_Series, StoreStatus_Success); 729 } 730 } 731 732 CreateDicomV2(RestApiPostCall & call,const Json::Value & request)733 static void CreateDicomV2(RestApiPostCall& call, 734 const Json::Value& request) 735 { 736 static const char* const SPECIFIC_CHARACTER_SET_2 = "SpecificCharacterSet"; 737 static const char* const TYPE = "Type"; 738 static const char* const VALUE = "Value"; 739 740 assert(request.isObject()); 741 ServerContext& context = OrthancRestApi::GetContext(call); 742 743 if (!request.isMember(TAGS) || 744 request[TAGS].type() != Json::objectValue) 745 { 746 throw OrthancException(ErrorCode_BadRequest); 747 } 748 749 ParsedDicomFile dicom(true); 750 751 { 752 Encoding encoding; 753 754 if (request[TAGS].isMember(SPECIFIC_CHARACTER_SET_2)) 755 { 756 const char* tmp = request[TAGS][SPECIFIC_CHARACTER_SET_2].asCString(); 757 if (!GetDicomEncoding(encoding, tmp)) 758 { 759 throw OrthancException(ErrorCode_ParameterOutOfRange, 760 "Unknown specific character set: " + std::string(tmp)); 761 } 762 } 763 else 764 { 765 encoding = GetDefaultDicomEncoding(); 766 } 767 768 dicom.SetEncoding(encoding); 769 } 770 771 ResourceType parentType = ResourceType_Instance; 772 773 if (request.isMember(PARENT)) 774 { 775 // Locate the parent tags 776 std::string parent = request[PARENT].asString(); 777 if (!context.GetIndex().LookupResourceType(parentType, parent)) 778 { 779 throw OrthancException(ErrorCode_CreateDicomBadParent); 780 } 781 782 if (parentType == ResourceType_Instance) 783 { 784 throw OrthancException(ErrorCode_CreateDicomParentIsInstance); 785 } 786 787 // Select one existing child instance of the parent resource, to 788 // retrieve all its tags 789 Json::Value siblingTags; 790 std::string siblingInstanceId; 791 792 { 793 // Retrieve all the instances of the parent resource 794 std::list<std::string> siblingInstances; 795 context.GetIndex().GetChildInstances(siblingInstances, parent); 796 797 if (siblingInstances.empty()) 798 { 799 // Error: No instance (should never happen) 800 throw OrthancException(ErrorCode_InternalError); 801 } 802 803 siblingInstanceId = siblingInstances.front(); 804 context.ReadDicomAsJson(siblingTags, siblingInstanceId); 805 } 806 807 808 // Choose the same encoding as the parent resource 809 { 810 static const char* const SPECIFIC_CHARACTER_SET = "0008,0005"; 811 812 if (siblingTags.isMember(SPECIFIC_CHARACTER_SET)) 813 { 814 Encoding encoding; 815 816 if (!siblingTags[SPECIFIC_CHARACTER_SET].isMember(VALUE) || 817 siblingTags[SPECIFIC_CHARACTER_SET][VALUE].type() != Json::stringValue || 818 !GetDicomEncoding(encoding, siblingTags[SPECIFIC_CHARACTER_SET][VALUE].asCString())) 819 { 820 LOG(WARNING) << "Instance with an incorrect Specific Character Set, " 821 << "using the default Orthanc encoding: " << siblingInstanceId; 822 encoding = GetDefaultDicomEncoding(); 823 } 824 825 dicom.SetEncoding(encoding); 826 } 827 } 828 829 830 // Retrieve the tags for all the parent modules 831 typedef std::set<DicomTag> ModuleTags; 832 ModuleTags moduleTags; 833 834 ResourceType type = parentType; 835 for (;;) 836 { 837 DicomTag::AddTagsForModule(moduleTags, GetModule(type)); 838 839 if (type == ResourceType_Patient) 840 { 841 break; // We're done 842 } 843 844 // Go up 845 std::string tmp; 846 if (!context.GetIndex().LookupParent(tmp, parent)) 847 { 848 throw OrthancException(ErrorCode_InternalError); 849 } 850 851 parent = tmp; 852 type = GetParentResourceType(type); 853 } 854 855 for (ModuleTags::const_iterator it = moduleTags.begin(); 856 it != moduleTags.end(); ++it) 857 { 858 std::string t = it->Format(); 859 if (siblingTags.isMember(t)) 860 { 861 const Json::Value& tag = siblingTags[t]; 862 if (tag[TYPE] == "Null") 863 { 864 dicom.ReplacePlainString(*it, ""); 865 } 866 else if (tag[TYPE] == "String") 867 { 868 std::string value = tag[VALUE].asString(); // This is an UTF-8 value (as it comes from JSON) 869 dicom.ReplacePlainString(*it, value); 870 } 871 } 872 } 873 } 874 875 876 bool decodeBinaryTags = true; 877 if (request.isMember(INTERPRET_BINARY_TAGS)) 878 { 879 const Json::Value& v = request[INTERPRET_BINARY_TAGS]; 880 if (v.type() != Json::booleanValue) 881 { 882 throw OrthancException(ErrorCode_BadRequest); 883 } 884 885 decodeBinaryTags = v.asBool(); 886 } 887 888 889 // New argument in Orthanc 1.6.0 890 std::string privateCreator; 891 if (request.isMember(PRIVATE_CREATOR)) 892 { 893 const Json::Value& v = request[PRIVATE_CREATOR]; 894 if (v.type() != Json::stringValue) 895 { 896 throw OrthancException(ErrorCode_BadRequest); 897 } 898 899 privateCreator = v.asString(); 900 } 901 else 902 { 903 OrthancConfiguration::ReaderLock lock; 904 privateCreator = lock.GetConfiguration().GetDefaultPrivateCreator(); 905 } 906 907 908 // New in Orthanc 1.9.0 909 bool force = false; 910 if (request.isMember(FORCE)) 911 { 912 const Json::Value& v = request[FORCE]; 913 if (v.type() != Json::booleanValue) 914 { 915 throw OrthancException(ErrorCode_BadRequest); 916 } 917 918 force = v.asBool(); 919 } 920 921 922 // Inject time-related information 923 std::string date, time; 924 SystemToolbox::GetNowDicom(date, time, true /* use UTC time (not local time) */); 925 dicom.ReplacePlainString(DICOM_TAG_ACQUISITION_DATE, date); 926 dicom.ReplacePlainString(DICOM_TAG_ACQUISITION_TIME, time); 927 dicom.ReplacePlainString(DICOM_TAG_CONTENT_DATE, date); 928 dicom.ReplacePlainString(DICOM_TAG_CONTENT_TIME, time); 929 dicom.ReplacePlainString(DICOM_TAG_INSTANCE_CREATION_DATE, date); 930 dicom.ReplacePlainString(DICOM_TAG_INSTANCE_CREATION_TIME, time); 931 932 if (parentType == ResourceType_Patient || 933 parentType == ResourceType_Study || 934 parentType == ResourceType_Instance /* no parent */) 935 { 936 dicom.ReplacePlainString(DICOM_TAG_SERIES_DATE, date); 937 dicom.ReplacePlainString(DICOM_TAG_SERIES_TIME, time); 938 } 939 940 if (parentType == ResourceType_Patient || 941 parentType == ResourceType_Instance /* no parent */) 942 { 943 dicom.ReplacePlainString(DICOM_TAG_STUDY_DATE, date); 944 dicom.ReplacePlainString(DICOM_TAG_STUDY_TIME, time); 945 } 946 947 948 InjectTags(dicom, request[TAGS], decodeBinaryTags, privateCreator, force); 949 950 951 // Inject the content (either an image, or a PDF file) 952 if (request.isMember(CONTENT)) 953 { 954 const Json::Value& content = request[CONTENT]; 955 956 if (content.type() == Json::stringValue) 957 { 958 dicom.EmbedContent(request[CONTENT].asString()); 959 960 } 961 else if (content.type() == Json::arrayValue) 962 { 963 if (content.size() > 0) 964 { 965 // Let's create a series instead of a single instance 966 CreateSeries(call, dicom, content, decodeBinaryTags, privateCreator, force); 967 return; 968 } 969 } 970 else 971 { 972 throw OrthancException(ErrorCode_CreateDicomUseDataUriScheme); 973 } 974 } 975 976 std::string id; 977 StoreCreatedInstance(id, call, dicom, true); 978 } 979 980 CreateDicom(RestApiPostCall & call)981 static void CreateDicom(RestApiPostCall& call) 982 { 983 if (call.IsDocumentation()) 984 { 985 call.GetDocumentation() 986 .SetTag("System") 987 .SetSummary("Create one DICOM instance") 988 .SetDescription("Create one DICOM instance, and store it into Orthanc") 989 .SetRequestField(TAGS, RestApiCallDocumentation::Type_JsonObject, 990 "Associative array containing the tags of the new instance to be created", true) 991 .SetRequestField(CONTENT, RestApiCallDocumentation::Type_String, 992 "This field can be used to embed an image (pixel data) or a PDF inside the created DICOM instance. " 993 "The PNG image, the JPEG image or the PDF file must be provided using their " 994 "[data URI scheme encoding](https://en.wikipedia.org/wiki/Data_URI_scheme). " 995 "This field can possibly contain a JSON array, in which case a DICOM series is created " 996 "containing one DICOM instance for each item in the `Content` field.", false) 997 .SetRequestField(PARENT, RestApiCallDocumentation::Type_String, 998 "If present, the newly created instance will be attached to the parent DICOM resource " 999 "whose Orthanc identifier is contained in this field. The DICOM tags of the parent " 1000 "modules in the DICOM hierarchy will be automatically copied to the newly created instance.", false) 1001 .SetRequestField(INTERPRET_BINARY_TAGS, RestApiCallDocumentation::Type_Boolean, 1002 "If some value in the `Tags` associative array is formatted according to some " 1003 "[data URI scheme encoding](https://en.wikipedia.org/wiki/Data_URI_scheme), " 1004 "whether this value is decoded to a binary value or kept as such (`true` by default)", false) 1005 .SetRequestField(PRIVATE_CREATOR, RestApiCallDocumentation::Type_String, 1006 "The private creator to be used for private tags in `Tags`", false) 1007 .SetRequestField(FORCE, RestApiCallDocumentation::Type_Boolean, 1008 "Avoid the consistency checks for the DICOM tags that enforce the DICOM model of the real-world. " 1009 "You can notably use this flag if you need to manually set the tags `StudyInstanceUID`, " 1010 "`SeriesInstanceUID`, or `SOPInstanceUID`. Be careful with this feature.", false) 1011 .SetAnswerField("ID", RestApiCallDocumentation::Type_String, "Orthanc identifier of the newly created instance") 1012 .SetAnswerField("Path", RestApiCallDocumentation::Type_String, "Path to access the instance in the REST API"); 1013 return; 1014 } 1015 1016 Json::Value request; 1017 if (!call.ParseJsonRequest(request) || 1018 !request.isObject()) 1019 { 1020 throw OrthancException(ErrorCode_BadRequest); 1021 } 1022 1023 if (request.isMember(TAGS)) 1024 { 1025 CreateDicomV2(call, request); 1026 } 1027 else 1028 { 1029 // Compatibility with Orthanc <= 0.9.3 1030 ParsedDicomFile dicom(true); 1031 CreateDicomV1(dicom, call, request); 1032 1033 std::string id; 1034 StoreCreatedInstance(id, call, dicom, true); 1035 } 1036 } 1037 1038 SplitStudy(RestApiPostCall & call)1039 static void SplitStudy(RestApiPostCall& call) 1040 { 1041 if (call.IsDocumentation()) 1042 { 1043 OrthancRestApi::DocumentSubmitCommandsJob(call); 1044 call.GetDocumentation() 1045 .SetTag("Studies") 1046 .SetSummary("Split study") 1047 .SetDescription("Start a new job so as to split the DICOM study whose Orthanc identifier is provided in the URL, " 1048 "by taking some of its children series or instances out of it and putting them into a brand new study " 1049 "(this new study is created by setting the `StudyInstanceUID` tag to a random identifier): " 1050 "https://book.orthanc-server.com/users/anonymization.html#splitting") 1051 .SetUriArgument("id", "Orthanc identifier of the study of interest") 1052 .SetRequestField(SERIES, RestApiCallDocumentation::Type_JsonListOfStrings, 1053 "The list of series to be separated from the parent study. " 1054 "These series must all be children of the same source study, that is specified in the URI.", false) 1055 .SetRequestField(REPLACE, RestApiCallDocumentation::Type_JsonObject, 1056 "Associative array to change the value of some DICOM tags in the new study. " 1057 "These tags must be part of the \"Patient Module Attributes\" or the \"General Study " 1058 "Module Attributes\", as specified by the DICOM 2011 standard in Tables C.7-1 and C.7-3.", false) 1059 .SetRequestField(REMOVE, RestApiCallDocumentation::Type_JsonListOfStrings, 1060 "List of tags that must be removed in the new study (from the same modules as in the `Replace` option)", false) 1061 .SetRequestField(KEEP_SOURCE, RestApiCallDocumentation::Type_Boolean, 1062 "If set to `true`, instructs Orthanc to keep a copy of the original series/instances in the source study. " 1063 "By default, the original series/instances are deleted from Orthanc.", false) 1064 .SetRequestField(INSTANCES, RestApiCallDocumentation::Type_JsonListOfStrings, 1065 "The list of instances to be separated from the parent study. " 1066 "These instances must all be children of the same source study, that is specified in the URI.", false); 1067 return; 1068 } 1069 1070 ServerContext& context = OrthancRestApi::GetContext(call); 1071 1072 Json::Value request; 1073 if (!call.ParseJsonRequest(request)) 1074 { 1075 // Bad JSON request 1076 throw OrthancException(ErrorCode_BadFileFormat); 1077 } 1078 1079 const std::string study = call.GetUriComponent("id", ""); 1080 1081 std::unique_ptr<SplitStudyJob> job(new SplitStudyJob(context, study)); 1082 job->SetOrigin(call); 1083 1084 bool ok = false; 1085 if (request.isMember(SERIES)) 1086 { 1087 std::vector<std::string> series; 1088 SerializationToolbox::ReadArrayOfStrings(series, request, SERIES); 1089 1090 for (size_t i = 0; i < series.size(); i++) 1091 { 1092 job->AddSourceSeries(series[i]); 1093 ok = true; 1094 } 1095 } 1096 1097 if (request.isMember(INSTANCES)) 1098 { 1099 std::vector<std::string> instances; 1100 SerializationToolbox::ReadArrayOfStrings(instances, request, INSTANCES); 1101 1102 for (size_t i = 0; i < instances.size(); i++) 1103 { 1104 job->AddSourceInstance(instances[i]); 1105 ok = true; 1106 } 1107 } 1108 1109 if (!ok) 1110 { 1111 throw OrthancException(ErrorCode_BadRequest, "Both the \"Series\" and the \"Instances\" fields are missing"); 1112 } 1113 1114 job->AddTrailingStep(); 1115 1116 SetKeepSource(*job, request); 1117 1118 if (request.isMember(REMOVE)) 1119 { 1120 if (request[REMOVE].type() != Json::arrayValue) 1121 { 1122 throw OrthancException(ErrorCode_BadFileFormat); 1123 } 1124 1125 for (Json::Value::ArrayIndex i = 0; i < request[REMOVE].size(); i++) 1126 { 1127 if (request[REMOVE][i].type() != Json::stringValue) 1128 { 1129 throw OrthancException(ErrorCode_BadFileFormat); 1130 } 1131 else 1132 { 1133 job->Remove(FromDcmtkBridge::ParseTag(request[REMOVE][i].asCString())); 1134 } 1135 } 1136 } 1137 1138 if (request.isMember(REPLACE)) 1139 { 1140 if (request[REPLACE].type() != Json::objectValue) 1141 { 1142 throw OrthancException(ErrorCode_BadFileFormat); 1143 } 1144 1145 Json::Value::Members tags = request[REPLACE].getMemberNames(); 1146 1147 for (size_t i = 0; i < tags.size(); i++) 1148 { 1149 const Json::Value& value = request[REPLACE][tags[i]]; 1150 1151 if (value.type() != Json::stringValue) 1152 { 1153 throw OrthancException(ErrorCode_BadFileFormat); 1154 } 1155 else 1156 { 1157 job->Replace(FromDcmtkBridge::ParseTag(tags[i]), value.asString()); 1158 } 1159 } 1160 } 1161 1162 OrthancRestApi::GetApi(call).SubmitCommandsJob 1163 (call, job.release(), true /* synchronous by default */, request); 1164 } 1165 1166 MergeStudy(RestApiPostCall & call)1167 static void MergeStudy(RestApiPostCall& call) 1168 { 1169 if (call.IsDocumentation()) 1170 { 1171 OrthancRestApi::DocumentSubmitCommandsJob(call); 1172 call.GetDocumentation() 1173 .SetTag("Studies") 1174 .SetSummary("Merge study") 1175 .SetDescription("Start a new job so as to move some DICOM resources into the DICOM study whose Orthanc identifier " 1176 "is provided in the URL: https://book.orthanc-server.com/users/anonymization.html#merging") 1177 .SetUriArgument("id", "Orthanc identifier of the study of interest") 1178 .SetRequestField(RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings, 1179 "The list of DICOM resources (studies, series, and/or instances) to be merged " 1180 "into the study of interest (mandatory option)", true) 1181 .SetRequestField(KEEP_SOURCE, RestApiCallDocumentation::Type_Boolean, 1182 "If set to `true`, instructs Orthanc to keep a copy of the original resources in their source study. " 1183 "By default, the original resources are deleted from Orthanc.", false); 1184 return; 1185 } 1186 1187 ServerContext& context = OrthancRestApi::GetContext(call); 1188 1189 Json::Value request; 1190 if (!call.ParseJsonRequest(request)) 1191 { 1192 // Bad JSON request 1193 throw OrthancException(ErrorCode_BadFileFormat); 1194 } 1195 1196 const std::string study = call.GetUriComponent("id", ""); 1197 1198 std::unique_ptr<MergeStudyJob> job(new MergeStudyJob(context, study)); 1199 job->SetOrigin(call); 1200 1201 std::vector<std::string> resources; 1202 SerializationToolbox::ReadArrayOfStrings(resources, request, RESOURCES); 1203 1204 for (size_t i = 0; i < resources.size(); i++) 1205 { 1206 job->AddSource(resources[i]); 1207 } 1208 1209 job->AddTrailingStep(); 1210 1211 SetKeepSource(*job, request); 1212 1213 OrthancRestApi::GetApi(call).SubmitCommandsJob 1214 (call, job.release(), true /* synchronous by default */, request); 1215 } 1216 1217 RegisterAnonymizeModify()1218 void OrthancRestApi::RegisterAnonymizeModify() 1219 { 1220 Register("/instances/{id}/modify", ModifyInstance); 1221 Register("/series/{id}/modify", ModifyResource<ResourceType_Series>); 1222 Register("/studies/{id}/modify", ModifyResource<ResourceType_Study>); 1223 Register("/patients/{id}/modify", ModifyResource<ResourceType_Patient>); 1224 Register("/tools/bulk-modify", BulkModify); 1225 1226 Register("/instances/{id}/anonymize", AnonymizeInstance); 1227 Register("/series/{id}/anonymize", AnonymizeResource<ResourceType_Series>); 1228 Register("/studies/{id}/anonymize", AnonymizeResource<ResourceType_Study>); 1229 Register("/patients/{id}/anonymize", AnonymizeResource<ResourceType_Patient>); 1230 Register("/tools/bulk-anonymize", BulkAnonymize); 1231 1232 Register("/tools/create-dicom", CreateDicom); 1233 1234 Register("/studies/{id}/split", SplitStudy); 1235 Register("/studies/{id}/merge", MergeStudy); 1236 } 1237 } 1238