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