comparison OrthancFramework/Sources/DicomParsing/DicomDirWriter.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/DicomParsing/DicomDirWriter.cpp@2a170a8f1faf
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
35
36
37 /*=========================================================================
38
39 This file is based on portions of the following project:
40
41 Program: DCMTK 3.6.0
42 Module: http://dicom.offis.de/dcmtk.php.en
43
44 Copyright (C) 1994-2011, OFFIS e.V.
45 All rights reserved.
46
47 This software and supporting documentation were developed by
48
49 OFFIS e.V.
50 R&D Division Health
51 Escherweg 2
52 26121 Oldenburg, Germany
53
54 Redistribution and use in source and binary forms, with or without
55 modification, are permitted provided that the following conditions
56 are met:
57
58 - Redistributions of source code must retain the above copyright
59 notice, this list of conditions and the following disclaimer.
60
61 - Redistributions in binary form must reproduce the above copyright
62 notice, this list of conditions and the following disclaimer in the
63 documentation and/or other materials provided with the distribution.
64
65 - Neither the name of OFFIS nor the names of its contributors may be
66 used to endorse or promote products derived from this software
67 without specific prior written permission.
68
69 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
70 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
71 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
72 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
73 HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
74 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
75 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
76 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
77 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
78 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
79 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
80
81 =========================================================================*/
82
83
84
85 /***
86
87 Validation:
88
89 # sudo apt-get install dicom3tools
90 # dciodvfy DICOMDIR 2>&1 | less
91 # dcentvfy DICOMDIR 2>&1 | less
92
93 http://www.dclunie.com/dicom3tools/dciodvfy.html
94
95 DICOMDIR viewer working with Wine under Linux:
96 http://www.microdicom.com/
97
98 ***/
99
100
101 #include "../PrecompiledHeaders.h"
102 #include "DicomDirWriter.h"
103
104 #include "FromDcmtkBridge.h"
105 #include "ToDcmtkBridge.h"
106
107 #include "../Compatibility.h"
108 #include "../Logging.h"
109 #include "../OrthancException.h"
110 #include "../TemporaryFile.h"
111 #include "../Toolbox.h"
112 #include "../SystemToolbox.h"
113
114 #include <dcmtk/dcmdata/dcdicdir.h>
115 #include <dcmtk/dcmdata/dcmetinf.h>
116 #include <dcmtk/dcmdata/dcdeftag.h>
117 #include <dcmtk/dcmdata/dcuid.h>
118 #include <dcmtk/dcmdata/dcddirif.h>
119 #include <dcmtk/dcmdata/dcvrui.h>
120 #include <dcmtk/dcmdata/dcsequen.h>
121 #include <dcmtk/dcmdata/dcostrmf.h>
122 #include "dcmtk/dcmdata/dcvrda.h" /* for class DcmDate */
123 #include "dcmtk/dcmdata/dcvrtm.h" /* for class DcmTime */
124
125 #include <memory>
126
127 namespace Orthanc
128 {
129 class DicomDirWriter::PImpl
130 {
131 private:
132 bool utc_;
133 std::string fileSetId_;
134 bool extendedSopClass_;
135 TemporaryFile file_;
136 std::unique_ptr<DcmDicomDir> dir_;
137
138 typedef std::pair<ResourceType, std::string> IndexKey;
139 typedef std::map<IndexKey, DcmDirectoryRecord* > Index;
140 Index index_;
141
142
143 DcmDicomDir& GetDicomDir()
144 {
145 if (dir_.get() == NULL)
146 {
147 dir_.reset(new DcmDicomDir(file_.GetPath().c_str(),
148 fileSetId_.c_str()));
149 //SetTagValue(dir_->getRootRecord(), DCM_SpecificCharacterSet, GetDicomSpecificCharacterSet(Encoding_Utf8));
150 }
151
152 return *dir_;
153 }
154
155
156 DcmDirectoryRecord& GetRoot()
157 {
158 return GetDicomDir().getRootRecord();
159 }
160
161
162 static bool GetUtf8TagValue(std::string& result,
163 DcmItem& source,
164 Encoding encoding,
165 bool hasCodeExtensions,
166 const DcmTagKey& key)
167 {
168 DcmElement* element = NULL;
169 result.clear();
170
171 if (source.findAndGetElement(key, element).good())
172 {
173 char* s = NULL;
174 if (element->isLeaf() &&
175 element->getString(s).good())
176 {
177 if (s != NULL)
178 {
179 result = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions);
180 }
181
182 return true;
183 }
184 }
185
186 return false;
187 }
188
189
190 static void SetTagValue(DcmDirectoryRecord& target,
191 const DcmTagKey& key,
192 const std::string& valueUtf8)
193 {
194 std::string s = Toolbox::ConvertFromUtf8(valueUtf8, Encoding_Ascii);
195
196 if (!target.putAndInsertString(key, s.c_str()).good())
197 {
198 throw OrthancException(ErrorCode_InternalError);
199 }
200 }
201
202
203
204 static bool CopyString(DcmDirectoryRecord& target,
205 DcmDataset& source,
206 Encoding encoding,
207 bool hasCodeExtensions,
208 const DcmTagKey& key,
209 bool optional,
210 bool copyEmpty)
211 {
212 if (optional &&
213 !source.tagExistsWithValue(key) &&
214 !(copyEmpty && source.tagExists(key)))
215 {
216 return false;
217 }
218
219 std::string value;
220 bool found = GetUtf8TagValue(value, source, encoding, hasCodeExtensions, key);
221
222 if (!found)
223 {
224 // We don't raise an exception if "!optional", even if this
225 // results in an invalid DICOM file
226 value.clear();
227 }
228
229 SetTagValue(target, key, value);
230 return found;
231 }
232
233
234 static void CopyStringType1(DcmDirectoryRecord& target,
235 DcmDataset& source,
236 Encoding encoding,
237 bool hasCodeExtensions,
238 const DcmTagKey& key)
239 {
240 CopyString(target, source, encoding, hasCodeExtensions, key, false, false);
241 }
242
243 static void CopyStringType1C(DcmDirectoryRecord& target,
244 DcmDataset& source,
245 Encoding encoding,
246 bool hasCodeExtensions,
247 const DcmTagKey& key)
248 {
249 CopyString(target, source, encoding, hasCodeExtensions, key, true, false);
250 }
251
252 static void CopyStringType2(DcmDirectoryRecord& target,
253 DcmDataset& source,
254 Encoding encoding,
255 bool hasCodeExtensions,
256 const DcmTagKey& key)
257 {
258 CopyString(target, source, encoding, hasCodeExtensions, key, false, true);
259 }
260
261 static void CopyStringType3(DcmDirectoryRecord& target,
262 DcmDataset& source,
263 Encoding encoding,
264 bool hasCodeExtensions,
265 const DcmTagKey& key)
266 {
267 CopyString(target, source, encoding, hasCodeExtensions, key, true, true);
268 }
269
270
271 public:
272 PImpl() :
273 utc_(true), // By default, use UTC (universal time, not local time)
274 fileSetId_("ORTHANC_MEDIA"),
275 extendedSopClass_(false)
276 {
277 }
278
279 bool IsUtcUsed() const
280 {
281 return utc_;
282 }
283
284
285 void SetUtcUsed(bool utc)
286 {
287 utc_ = utc;
288 }
289
290 void EnableExtendedSopClass(bool enable)
291 {
292 if (enable)
293 {
294 LOG(WARNING) << "Generating a DICOMDIR with type 3 attributes, "
295 << "which leads to an Extended SOP Class";
296 }
297
298 extendedSopClass_ = enable;
299 }
300
301 bool IsExtendedSopClass() const
302 {
303 return extendedSopClass_;
304 }
305
306 void FillPatient(DcmDirectoryRecord& record,
307 DcmDataset& dicom,
308 Encoding encoding,
309 bool hasCodeExtensions)
310 {
311 // cf. "DicomDirInterface::buildPatientRecord()"
312
313 CopyStringType1C(record, dicom, encoding, hasCodeExtensions, DCM_PatientID);
314 CopyStringType2(record, dicom, encoding, hasCodeExtensions, DCM_PatientName);
315 }
316
317 void FillStudy(DcmDirectoryRecord& record,
318 DcmDataset& dicom,
319 Encoding encoding,
320 bool hasCodeExtensions)
321 {
322 // cf. "DicomDirInterface::buildStudyRecord()"
323
324 std::string nowDate, nowTime;
325 SystemToolbox::GetNowDicom(nowDate, nowTime, utc_);
326
327 std::string studyDate;
328 if (!GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_StudyDate) &&
329 !GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_SeriesDate) &&
330 !GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_AcquisitionDate) &&
331 !GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_ContentDate))
332 {
333 studyDate = nowDate;
334 }
335
336 std::string studyTime;
337 if (!GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, DCM_StudyTime) &&
338 !GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, DCM_SeriesTime) &&
339 !GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, DCM_AcquisitionTime) &&
340 !GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, DCM_ContentTime))
341 {
342 studyTime = nowTime;
343 }
344
345 /* copy attribute values from dataset to study record */
346 SetTagValue(record, DCM_StudyDate, studyDate);
347 SetTagValue(record, DCM_StudyTime, studyTime);
348 CopyStringType2(record, dicom, encoding, hasCodeExtensions, DCM_StudyDescription);
349 CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_StudyInstanceUID);
350 /* use type 1C instead of 1 in order to avoid unwanted overwriting */
351 CopyStringType1C(record, dicom, encoding, hasCodeExtensions, DCM_StudyID);
352 CopyStringType2(record, dicom, encoding, hasCodeExtensions, DCM_AccessionNumber);
353 }
354
355 void FillSeries(DcmDirectoryRecord& record,
356 DcmDataset& dicom,
357 Encoding encoding,
358 bool hasCodeExtensions)
359 {
360 // cf. "DicomDirInterface::buildSeriesRecord()"
361
362 /* copy attribute values from dataset to series record */
363 CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_Modality);
364 CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_SeriesInstanceUID);
365 /* use type 1C instead of 1 in order to avoid unwanted overwriting */
366 CopyStringType1C(record, dicom, encoding, hasCodeExtensions, DCM_SeriesNumber);
367
368 // Add extended (non-standard) type 3 tags, those are not generated by DCMTK
369 // http://dicom.nema.org/medical/Dicom/2016a/output/chtml/part02/sect_7.3.html
370 // https://groups.google.com/d/msg/orthanc-users/Y7LOvZMDeoc/9cp3kDgxAwAJ
371 if (extendedSopClass_)
372 {
373 CopyStringType3(record, dicom, encoding, hasCodeExtensions, DCM_SeriesDescription);
374 }
375 }
376
377 void FillInstance(DcmDirectoryRecord& record,
378 DcmDataset& dicom,
379 Encoding encoding,
380 bool hasCodeExtensions,
381 DcmMetaInfo& metaInfo,
382 const char* path)
383 {
384 // cf. "DicomDirInterface::buildImageRecord()"
385
386 /* copy attribute values from dataset to image record */
387 CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_InstanceNumber);
388 //CopyElementType1C(record, dicom, encoding, hasCodeExtensions, DCM_ImageType);
389
390 // REMOVED since 0.9.7: copyElementType1C(dicom, DCM_ReferencedImageSequence, record);
391
392 std::string sopClassUid, sopInstanceUid, transferSyntaxUid;
393 if (!GetUtf8TagValue(sopClassUid, dicom, encoding, hasCodeExtensions, DCM_SOPClassUID) ||
394 !GetUtf8TagValue(sopInstanceUid, dicom, encoding, hasCodeExtensions, DCM_SOPInstanceUID) ||
395 !GetUtf8TagValue(transferSyntaxUid, metaInfo, encoding, hasCodeExtensions, DCM_TransferSyntaxUID))
396 {
397 throw OrthancException(ErrorCode_BadFileFormat);
398 }
399
400 SetTagValue(record, DCM_ReferencedFileID, path);
401 SetTagValue(record, DCM_ReferencedSOPClassUIDInFile, sopClassUid);
402 SetTagValue(record, DCM_ReferencedSOPInstanceUIDInFile, sopInstanceUid);
403 SetTagValue(record, DCM_ReferencedTransferSyntaxUIDInFile, transferSyntaxUid);
404 }
405
406
407
408 bool CreateResource(DcmDirectoryRecord*& target,
409 ResourceType level,
410 ParsedDicomFile& dicom,
411 const char* filename,
412 const char* path)
413 {
414 DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset();
415
416 bool hasCodeExtensions;
417 Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
418
419 bool found;
420 std::string id;
421 E_DirRecType type;
422
423 switch (level)
424 {
425 case ResourceType_Patient:
426 if (!GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, DCM_PatientID))
427 {
428 // Be tolerant about missing patient ID. Fixes issue #124
429 // (GET /studies/ID/media fails for certain dicom file).
430 id = "";
431 }
432
433 found = true;
434 type = ERT_Patient;
435 break;
436
437 case ResourceType_Study:
438 found = GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, DCM_StudyInstanceUID);
439 type = ERT_Study;
440 break;
441
442 case ResourceType_Series:
443 found = GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, DCM_SeriesInstanceUID);
444 type = ERT_Series;
445 break;
446
447 case ResourceType_Instance:
448 found = GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, DCM_SOPInstanceUID);
449 type = ERT_Image;
450 break;
451
452 default:
453 throw OrthancException(ErrorCode_InternalError);
454 }
455
456 if (!found)
457 {
458 throw OrthancException(ErrorCode_BadFileFormat);
459 }
460
461 IndexKey key = std::make_pair(level, std::string(id.c_str()));
462 Index::iterator it = index_.find(key);
463
464 if (it != index_.end())
465 {
466 target = it->second;
467 return false; // Already existing
468 }
469
470 std::unique_ptr<DcmDirectoryRecord> record(new DcmDirectoryRecord(type, NULL, filename));
471
472 switch (level)
473 {
474 case ResourceType_Patient:
475 FillPatient(*record, dataset, encoding, hasCodeExtensions);
476 break;
477
478 case ResourceType_Study:
479 FillStudy(*record, dataset, encoding, hasCodeExtensions);
480 break;
481
482 case ResourceType_Series:
483 FillSeries(*record, dataset, encoding, hasCodeExtensions);
484 break;
485
486 case ResourceType_Instance:
487 FillInstance(*record, dataset, encoding, hasCodeExtensions, *dicom.GetDcmtkObject().getMetaInfo(), path);
488 break;
489
490 default:
491 throw OrthancException(ErrorCode_InternalError);
492 }
493
494 CopyStringType1C(*record, dataset, encoding, hasCodeExtensions, DCM_SpecificCharacterSet);
495
496 target = record.get();
497 GetRoot().insertSub(record.release());
498 index_[key] = target;
499
500 return true; // Newly created
501 }
502
503 void Read(std::string& s)
504 {
505 if (!GetDicomDir().write(DICOMDIR_DEFAULT_TRANSFERSYNTAX,
506 EET_UndefinedLength /*encodingType*/,
507 EGL_withoutGL /*groupLength*/).good())
508 {
509 throw OrthancException(ErrorCode_InternalError);
510 }
511
512 file_.Read(s);
513 }
514
515 void SetFileSetId(const std::string& id)
516 {
517 dir_.reset(NULL);
518 fileSetId_ = id;
519 }
520 };
521
522
523 DicomDirWriter::DicomDirWriter() : pimpl_(new PImpl)
524 {
525 }
526
527 void DicomDirWriter::SetUtcUsed(bool utc)
528 {
529 pimpl_->SetUtcUsed(utc);
530 }
531
532 bool DicomDirWriter::IsUtcUsed() const
533 {
534 return pimpl_->IsUtcUsed();
535 }
536
537 void DicomDirWriter::SetFileSetId(const std::string& id)
538 {
539 pimpl_->SetFileSetId(id);
540 }
541
542 void DicomDirWriter::Add(const std::string& directory,
543 const std::string& filename,
544 ParsedDicomFile& dicom)
545 {
546 std::string path;
547 if (directory.empty())
548 {
549 path = filename;
550 }
551 else
552 {
553 if (directory[directory.length() - 1] == '/' ||
554 directory[directory.length() - 1] == '\\')
555 {
556 throw OrthancException(ErrorCode_ParameterOutOfRange);
557 }
558
559 path = directory + '\\' + filename;
560 }
561
562 DcmDirectoryRecord* instance;
563 bool isNewInstance = pimpl_->CreateResource(instance, ResourceType_Instance, dicom, filename.c_str(), path.c_str());
564 if (isNewInstance)
565 {
566 DcmDirectoryRecord* series;
567 bool isNewSeries = pimpl_->CreateResource(series, ResourceType_Series, dicom, filename.c_str(), NULL);
568 series->insertSub(instance);
569
570 if (isNewSeries)
571 {
572 DcmDirectoryRecord* study;
573 bool isNewStudy = pimpl_->CreateResource(study, ResourceType_Study, dicom, filename.c_str(), NULL);
574 study->insertSub(series);
575
576 if (isNewStudy)
577 {
578 DcmDirectoryRecord* patient;
579 pimpl_->CreateResource(patient, ResourceType_Patient, dicom, filename.c_str(), NULL);
580 patient->insertSub(study);
581 }
582 }
583 }
584 }
585
586 void DicomDirWriter::Encode(std::string& target)
587 {
588 pimpl_->Read(target);
589 }
590
591
592 void DicomDirWriter::EnableExtendedSopClass(bool enable)
593 {
594 pimpl_->EnableExtendedSopClass(enable);
595 }
596
597
598 bool DicomDirWriter::IsExtendedSopClass() const
599 {
600 return pimpl_->IsExtendedSopClass();
601 }
602 }