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