Mercurial > hg > orthanc
comparison OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp @ 4044:d25f4c0fa160 framework
splitting code into OrthancFramework and OrthancServer
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 10 Jun 2020 20:30:34 +0200 |
parents | Core/DicomNetworking/DicomStoreUserConnection.cpp@5fe8c6d3212e |
children | bf7b9edf6b81 |
comparison
equal
deleted
inserted
replaced
4043:6c6239aec462 | 4044:d25f4c0fa160 |
---|---|
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-2020 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 "../PrecompiledHeaders.h" | |
35 #include "DicomStoreUserConnection.h" | |
36 | |
37 #include "../DicomParsing/FromDcmtkBridge.h" | |
38 #include "../DicomParsing/ParsedDicomFile.h" | |
39 #include "../Logging.h" | |
40 #include "../OrthancException.h" | |
41 #include "DicomAssociation.h" | |
42 | |
43 #include <dcmtk/dcmdata/dcdeftag.h> | |
44 | |
45 | |
46 namespace Orthanc | |
47 { | |
48 bool DicomStoreUserConnection::ProposeStorageClass(const std::string& sopClassUid, | |
49 const std::set<DicomTransferSyntax>& syntaxes) | |
50 { | |
51 // Default transfer syntax for DICOM | |
52 const bool addLittleEndianImplicit = ( | |
53 proposeUncompressedSyntaxes_ && | |
54 syntaxes.find(DicomTransferSyntax_LittleEndianImplicit) == syntaxes.end()); | |
55 | |
56 const bool addLittleEndianExplicit = ( | |
57 proposeUncompressedSyntaxes_ && | |
58 syntaxes.find(DicomTransferSyntax_LittleEndianExplicit) == syntaxes.end()); | |
59 | |
60 const bool addBigEndianExplicit = ( | |
61 proposeUncompressedSyntaxes_ && | |
62 proposeRetiredBigEndian_ && | |
63 syntaxes.find(DicomTransferSyntax_BigEndianExplicit) == syntaxes.end()); | |
64 | |
65 size_t requiredCount = syntaxes.size(); | |
66 if (addLittleEndianImplicit) | |
67 { | |
68 requiredCount += 1; | |
69 } | |
70 | |
71 if (addLittleEndianExplicit || | |
72 addBigEndianExplicit) | |
73 { | |
74 requiredCount += 1; | |
75 } | |
76 | |
77 if (association_->GetRemainingPropositions() <= requiredCount) | |
78 { | |
79 return false; // Not enough room | |
80 } | |
81 else | |
82 { | |
83 for (std::set<DicomTransferSyntax>::const_iterator | |
84 it = syntaxes.begin(); it != syntaxes.end(); ++it) | |
85 { | |
86 association_->ProposePresentationContext(sopClassUid, *it); | |
87 proposedOriginalClasses_.insert(std::make_pair(sopClassUid, *it)); | |
88 } | |
89 | |
90 if (addLittleEndianImplicit) | |
91 { | |
92 association_->ProposePresentationContext(sopClassUid, DicomTransferSyntax_LittleEndianImplicit); | |
93 proposedOriginalClasses_.insert(std::make_pair(sopClassUid, DicomTransferSyntax_LittleEndianImplicit)); | |
94 } | |
95 | |
96 if (addLittleEndianExplicit || | |
97 addBigEndianExplicit) | |
98 { | |
99 std::set<DicomTransferSyntax> uncompressed; | |
100 | |
101 if (addLittleEndianExplicit) | |
102 { | |
103 uncompressed.insert(DicomTransferSyntax_LittleEndianExplicit); | |
104 } | |
105 | |
106 if (addBigEndianExplicit) | |
107 { | |
108 uncompressed.insert(DicomTransferSyntax_BigEndianExplicit); | |
109 } | |
110 | |
111 association_->ProposePresentationContext(sopClassUid, uncompressed); | |
112 | |
113 assert(!uncompressed.empty()); | |
114 if (addLittleEndianExplicit ^ addBigEndianExplicit) | |
115 { | |
116 // Only one transfer syntax was proposed for this presentation context | |
117 assert(uncompressed.size() == 1); | |
118 proposedOriginalClasses_.insert(std::make_pair(sopClassUid, *uncompressed.begin())); | |
119 } | |
120 } | |
121 | |
122 return true; | |
123 } | |
124 } | |
125 | |
126 | |
127 bool DicomStoreUserConnection::LookupPresentationContext( | |
128 uint8_t& presentationContextId, | |
129 const std::string& sopClassUid, | |
130 DicomTransferSyntax transferSyntax) | |
131 { | |
132 typedef std::map<DicomTransferSyntax, uint8_t> PresentationContexts; | |
133 | |
134 PresentationContexts pc; | |
135 if (association_->IsOpen() && | |
136 association_->LookupAcceptedPresentationContext(pc, sopClassUid)) | |
137 { | |
138 PresentationContexts::const_iterator found = pc.find(transferSyntax); | |
139 if (found != pc.end()) | |
140 { | |
141 presentationContextId = found->second; | |
142 return true; | |
143 } | |
144 } | |
145 | |
146 return false; | |
147 } | |
148 | |
149 | |
150 DicomStoreUserConnection::DicomStoreUserConnection( | |
151 const DicomAssociationParameters& params) : | |
152 parameters_(params), | |
153 association_(new DicomAssociation), | |
154 proposeCommonClasses_(true), | |
155 proposeUncompressedSyntaxes_(true), | |
156 proposeRetiredBigEndian_(false) | |
157 { | |
158 } | |
159 | |
160 | |
161 void DicomStoreUserConnection::RegisterStorageClass(const std::string& sopClassUid, | |
162 DicomTransferSyntax syntax) | |
163 { | |
164 RegisteredClasses::iterator found = registeredClasses_.find(sopClassUid); | |
165 | |
166 if (found == registeredClasses_.end()) | |
167 { | |
168 std::set<DicomTransferSyntax> ts; | |
169 ts.insert(syntax); | |
170 registeredClasses_[sopClassUid] = ts; | |
171 } | |
172 else | |
173 { | |
174 found->second.insert(syntax); | |
175 } | |
176 } | |
177 | |
178 | |
179 void DicomStoreUserConnection::LookupParameters(std::string& sopClassUid, | |
180 std::string& sopInstanceUid, | |
181 DicomTransferSyntax& transferSyntax, | |
182 DcmFileFormat& dicom) | |
183 { | |
184 if (dicom.getDataset() == NULL) | |
185 { | |
186 throw OrthancException(ErrorCode_InternalError); | |
187 } | |
188 | |
189 OFString a, b; | |
190 if (!dicom.getDataset()->findAndGetOFString(DCM_SOPClassUID, a).good() || | |
191 !dicom.getDataset()->findAndGetOFString(DCM_SOPInstanceUID, b).good()) | |
192 { | |
193 throw OrthancException(ErrorCode_NoSopClassOrInstance, | |
194 "Unable to determine the SOP class/instance for C-STORE with AET " + | |
195 parameters_.GetRemoteModality().GetApplicationEntityTitle()); | |
196 } | |
197 | |
198 sopClassUid.assign(a.c_str()); | |
199 sopInstanceUid.assign(b.c_str()); | |
200 | |
201 if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax, dicom)) | |
202 { | |
203 throw OrthancException(ErrorCode_InternalError, | |
204 "Unknown transfer syntax from DCMTK"); | |
205 } | |
206 } | |
207 | |
208 | |
209 bool DicomStoreUserConnection::NegotiatePresentationContext( | |
210 uint8_t& presentationContextId, | |
211 const std::string& sopClassUid, | |
212 DicomTransferSyntax transferSyntax) | |
213 { | |
214 /** | |
215 * Step 1: Check whether this presentation context is already | |
216 * available in the previously negotiated assocation. | |
217 **/ | |
218 | |
219 if (LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax)) | |
220 { | |
221 return true; | |
222 } | |
223 | |
224 // The association must be re-negotiated | |
225 if (association_->IsOpen()) | |
226 { | |
227 LOG(INFO) << "Re-negotiating DICOM association with " | |
228 << parameters_.GetRemoteModality().GetApplicationEntityTitle(); | |
229 | |
230 if (proposedOriginalClasses_.find(std::make_pair(sopClassUid, transferSyntax)) != | |
231 proposedOriginalClasses_.end()) | |
232 { | |
233 LOG(INFO) << "The remote modality has already rejected SOP class UID \"" | |
234 << sopClassUid << "\" with transfer syntax \"" | |
235 << GetTransferSyntaxUid(transferSyntax) << "\", don't renegotiate"; | |
236 return false; | |
237 } | |
238 } | |
239 | |
240 association_->ClearPresentationContexts(); | |
241 proposedOriginalClasses_.clear(); | |
242 RegisterStorageClass(sopClassUid, transferSyntax); // (*) | |
243 | |
244 | |
245 /** | |
246 * Step 2: Propose at least the mandatory SOP class. | |
247 **/ | |
248 | |
249 { | |
250 RegisteredClasses::const_iterator mandatory = registeredClasses_.find(sopClassUid); | |
251 | |
252 if (mandatory == registeredClasses_.end() || | |
253 mandatory->second.find(transferSyntax) == mandatory->second.end()) | |
254 { | |
255 // Should never fail because of (*) | |
256 throw OrthancException(ErrorCode_InternalError); | |
257 } | |
258 | |
259 if (!ProposeStorageClass(sopClassUid, mandatory->second)) | |
260 { | |
261 // Should never happen in real life: There are no more than | |
262 // 128 transfer syntaxes in DICOM! | |
263 throw OrthancException(ErrorCode_InternalError, | |
264 "Too many transfer syntaxes for SOP class UID: " + sopClassUid); | |
265 } | |
266 } | |
267 | |
268 | |
269 /** | |
270 * Step 3: Propose all the previously spotted SOP classes, as | |
271 * registered through the "RegisterStorageClass()" method. | |
272 **/ | |
273 | |
274 for (RegisteredClasses::const_iterator it = registeredClasses_.begin(); | |
275 it != registeredClasses_.end(); ++it) | |
276 { | |
277 if (it->first != sopClassUid) | |
278 { | |
279 ProposeStorageClass(it->first, it->second); | |
280 } | |
281 } | |
282 | |
283 | |
284 /** | |
285 * Step 4: As long as there is room left in the proposed | |
286 * presentation contexts, propose the uncompressed transfer syntaxes | |
287 * for the most common SOP classes, as can be found in the | |
288 * "dcmShortSCUStorageSOPClassUIDs" array from DCMTK. The | |
289 * preferred transfer syntax is "LittleEndianImplicit". | |
290 **/ | |
291 | |
292 if (proposeCommonClasses_) | |
293 { | |
294 // The method "ProposeStorageClass()" will automatically add | |
295 // "LittleEndianImplicit" | |
296 std::set<DicomTransferSyntax> ts; | |
297 | |
298 for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs; i++) | |
299 { | |
300 std::string c(dcmShortSCUStorageSOPClassUIDs[i]); | |
301 | |
302 if (c != sopClassUid && | |
303 registeredClasses_.find(c) == registeredClasses_.end()) | |
304 { | |
305 ProposeStorageClass(c, ts); | |
306 } | |
307 } | |
308 } | |
309 | |
310 | |
311 /** | |
312 * Step 5: Open the association, and check whether the pair (SOP | |
313 * class UID, transfer syntax) was accepted by the remote host. | |
314 **/ | |
315 | |
316 association_->Open(parameters_); | |
317 return LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax); | |
318 } | |
319 | |
320 | |
321 void DicomStoreUserConnection::Store(std::string& sopClassUid, | |
322 std::string& sopInstanceUid, | |
323 DcmFileFormat& dicom, | |
324 bool hasMoveOriginator, | |
325 const std::string& moveOriginatorAET, | |
326 uint16_t moveOriginatorID) | |
327 { | |
328 DicomTransferSyntax transferSyntax; | |
329 LookupParameters(sopClassUid, sopInstanceUid, transferSyntax, dicom); | |
330 | |
331 uint8_t presID; | |
332 if (!NegotiatePresentationContext(presID, sopClassUid, transferSyntax)) | |
333 { | |
334 throw OrthancException(ErrorCode_NetworkProtocol, | |
335 "No valid presentation context was negotiated for " | |
336 "SOP class UID [" + sopClassUid + "] and transfer " | |
337 "syntax [" + GetTransferSyntaxUid(transferSyntax) + "] " | |
338 "while sending to modality [" + | |
339 parameters_.GetRemoteModality().GetApplicationEntityTitle() + "]"); | |
340 } | |
341 | |
342 // Prepare the transmission of data | |
343 T_DIMSE_C_StoreRQ request; | |
344 memset(&request, 0, sizeof(request)); | |
345 request.MessageID = association_->GetDcmtkAssociation().nextMsgID++; | |
346 strncpy(request.AffectedSOPClassUID, sopClassUid.c_str(), DIC_UI_LEN); | |
347 request.Priority = DIMSE_PRIORITY_MEDIUM; | |
348 request.DataSetType = DIMSE_DATASET_PRESENT; | |
349 strncpy(request.AffectedSOPInstanceUID, sopInstanceUid.c_str(), DIC_UI_LEN); | |
350 | |
351 if (hasMoveOriginator) | |
352 { | |
353 strncpy(request.MoveOriginatorApplicationEntityTitle, | |
354 moveOriginatorAET.c_str(), DIC_AE_LEN); | |
355 request.opts = O_STORE_MOVEORIGINATORAETITLE; | |
356 | |
357 request.MoveOriginatorID = moveOriginatorID; // The type DIC_US is an alias for uint16_t | |
358 request.opts |= O_STORE_MOVEORIGINATORID; | |
359 } | |
360 | |
361 if (dicom.getDataset() == NULL) | |
362 { | |
363 throw OrthancException(ErrorCode_InternalError); | |
364 } | |
365 | |
366 // Finally conduct transmission of data | |
367 T_DIMSE_C_StoreRSP response; | |
368 DcmDataset* statusDetail = NULL; | |
369 DicomAssociation::CheckCondition( | |
370 DIMSE_storeUser(&association_->GetDcmtkAssociation(), presID, &request, | |
371 NULL, dicom.getDataset(), /*progressCallback*/ NULL, NULL, | |
372 /*opt_blockMode*/ (GetParameters().HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), | |
373 /*opt_dimse_timeout*/ GetParameters().GetTimeout(), | |
374 &response, &statusDetail, NULL), | |
375 GetParameters(), "C-STORE"); | |
376 | |
377 if (statusDetail != NULL) | |
378 { | |
379 delete statusDetail; | |
380 } | |
381 | |
382 /** | |
383 * New in Orthanc 1.6.0: Deal with failures during C-STORE. | |
384 * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_B.2.3.html#table_B.2-1 | |
385 **/ | |
386 | |
387 if (response.DimseStatus != 0x0000 && // Success | |
388 response.DimseStatus != 0xB000 && // Warning - Coercion of Data Elements | |
389 response.DimseStatus != 0xB007 && // Warning - Data Set does not match SOP Class | |
390 response.DimseStatus != 0xB006) // Warning - Elements Discarded | |
391 { | |
392 char buf[16]; | |
393 sprintf(buf, "%04X", response.DimseStatus); | |
394 throw OrthancException(ErrorCode_NetworkProtocol, | |
395 "C-STORE SCU to AET \"" + | |
396 GetParameters().GetRemoteModality().GetApplicationEntityTitle() + | |
397 "\" has failed with DIMSE status 0x" + buf); | |
398 } | |
399 } | |
400 | |
401 | |
402 void DicomStoreUserConnection::Store(std::string& sopClassUid, | |
403 std::string& sopInstanceUid, | |
404 const void* buffer, | |
405 size_t size, | |
406 bool hasMoveOriginator, | |
407 const std::string& moveOriginatorAET, | |
408 uint16_t moveOriginatorID) | |
409 { | |
410 std::unique_ptr<DcmFileFormat> dicom( | |
411 FromDcmtkBridge::LoadFromMemoryBuffer(buffer, size)); | |
412 | |
413 if (dicom.get() == NULL) | |
414 { | |
415 throw OrthancException(ErrorCode_InternalError); | |
416 } | |
417 | |
418 Store(sopClassUid, sopInstanceUid, *dicom, hasMoveOriginator, moveOriginatorAET, moveOriginatorID); | |
419 } | |
420 | |
421 | |
422 void DicomStoreUserConnection::LookupTranscoding(std::set<DicomTransferSyntax>& acceptedSyntaxes, | |
423 const std::string& sopClassUid, | |
424 DicomTransferSyntax sourceSyntax) | |
425 { | |
426 acceptedSyntaxes.clear(); | |
427 | |
428 // Make sure a negotiation has already occurred for this transfer | |
429 // syntax. We don't use the return code: Transcoding is possible | |
430 // even if the "sourceSyntax" is not supported. | |
431 uint8_t presID; | |
432 NegotiatePresentationContext(presID, sopClassUid, sourceSyntax); | |
433 | |
434 std::map<DicomTransferSyntax, uint8_t> contexts; | |
435 if (association_->LookupAcceptedPresentationContext(contexts, sopClassUid)) | |
436 { | |
437 for (std::map<DicomTransferSyntax, uint8_t>::const_iterator | |
438 it = contexts.begin(); it != contexts.end(); ++it) | |
439 { | |
440 acceptedSyntaxes.insert(it->first); | |
441 } | |
442 } | |
443 } | |
444 | |
445 | |
446 void DicomStoreUserConnection::Transcode(std::string& sopClassUid /* out */, | |
447 std::string& sopInstanceUid /* out */, | |
448 IDicomTranscoder& transcoder, | |
449 const void* buffer, | |
450 size_t size, | |
451 bool hasMoveOriginator, | |
452 const std::string& moveOriginatorAET, | |
453 uint16_t moveOriginatorID) | |
454 { | |
455 std::unique_ptr<DcmFileFormat> dicom(FromDcmtkBridge::LoadFromMemoryBuffer(buffer, size)); | |
456 if (dicom.get() == NULL || | |
457 dicom->getDataset() == NULL) | |
458 { | |
459 throw OrthancException(ErrorCode_NullPointer); | |
460 } | |
461 | |
462 DicomTransferSyntax inputSyntax; | |
463 LookupParameters(sopClassUid, sopInstanceUid, inputSyntax, *dicom); | |
464 | |
465 std::set<DicomTransferSyntax> accepted; | |
466 LookupTranscoding(accepted, sopClassUid, inputSyntax); | |
467 | |
468 if (accepted.find(inputSyntax) != accepted.end()) | |
469 { | |
470 // No need for transcoding | |
471 Store(sopClassUid, sopInstanceUid, *dicom, | |
472 hasMoveOriginator, moveOriginatorAET, moveOriginatorID); | |
473 } | |
474 else | |
475 { | |
476 // Transcoding is needed | |
477 std::set<DicomTransferSyntax> uncompressedSyntaxes; | |
478 | |
479 if (accepted.find(DicomTransferSyntax_LittleEndianImplicit) != accepted.end()) | |
480 { | |
481 uncompressedSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit); | |
482 } | |
483 | |
484 if (accepted.find(DicomTransferSyntax_LittleEndianExplicit) != accepted.end()) | |
485 { | |
486 uncompressedSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit); | |
487 } | |
488 | |
489 if (accepted.find(DicomTransferSyntax_BigEndianExplicit) != accepted.end()) | |
490 { | |
491 uncompressedSyntaxes.insert(DicomTransferSyntax_BigEndianExplicit); | |
492 } | |
493 | |
494 IDicomTranscoder::DicomImage source; | |
495 source.AcquireParsed(dicom.release()); | |
496 source.SetExternalBuffer(buffer, size); | |
497 | |
498 const std::string sourceUid = IDicomTranscoder::GetSopInstanceUid(source.GetParsed()); | |
499 | |
500 IDicomTranscoder::DicomImage transcoded; | |
501 if (transcoder.Transcode(transcoded, source, uncompressedSyntaxes, false)) | |
502 { | |
503 if (sourceUid != IDicomTranscoder::GetSopInstanceUid(transcoded.GetParsed())) | |
504 { | |
505 throw OrthancException(ErrorCode_Plugin, "The transcoder has changed the SOP " | |
506 "instance UID while transcoding to an uncompressed transfer syntax"); | |
507 } | |
508 else | |
509 { | |
510 DicomTransferSyntax transcodedSyntax; | |
511 | |
512 // Sanity check | |
513 if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transcodedSyntax, transcoded.GetParsed()) || | |
514 accepted.find(transcodedSyntax) == accepted.end()) | |
515 { | |
516 throw OrthancException(ErrorCode_InternalError); | |
517 } | |
518 else | |
519 { | |
520 Store(sopClassUid, sopInstanceUid, transcoded.GetParsed(), | |
521 hasMoveOriginator, moveOriginatorAET, moveOriginatorID); | |
522 } | |
523 } | |
524 } | |
525 } | |
526 } | |
527 } |