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