comparison OrthancServer/DicomDirWriter.cpp @ 1121:82567bac5e25

Creation of ZIP archives for media storage, with DICOMDIR
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 05 Sep 2014 14:28:43 +0200
parents
children baac89e6cc4b
comparison
equal deleted inserted replaced
1120:009dce4ea2f6 1121:82567bac5e25
1 /**
2 * Orthanc - A Lightweight, RESTful DICOM Store
3 * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
4 * Belgium
5 *
6 * This program is free software: you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License as
8 * published by the Free Software Foundation, either version 3 of the
9 * License, or (at your option) any later version.
10 *
11 * In addition, as a special exception, the copyright holders of this
12 * program give permission to link the code of its release with the
13 * OpenSSL project's "OpenSSL" library (or with modified versions of it
14 * that use the same license as the "OpenSSL" library), and distribute
15 * the linked executables. You must obey the GNU General Public License
16 * in all respects for all of the code used other than "OpenSSL". If you
17 * modify file(s) with this exception, you may extend this exception to
18 * your version of the file(s), but you are not obligated to do so. If
19 * you do not wish to do so, delete this exception statement from your
20 * version. If you delete this exception statement from all source files
21 * in the program, then also delete it here.
22 *
23 * This program is distributed in the hope that it will be useful, but
24 * WITHOUT ANY WARRANTY; without even the implied warranty of
25 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
26 * General Public License for more details.
27 *
28 * You should have received a copy of the GNU General Public License
29 * along with this program. If not, see <http://www.gnu.org/licenses/>.
30 **/
31
32
33
34
35
36 /*=========================================================================
37
38 This file is based on portions of the following project:
39
40 Program: DCMTK 3.6.0
41 Module: http://dicom.offis.de/dcmtk.php.en
42
43 Copyright (C) 1994-2011, OFFIS e.V.
44 All rights reserved.
45
46 This software and supporting documentation were developed by
47
48 OFFIS e.V.
49 R&D Division Health
50 Escherweg 2
51 26121 Oldenburg, Germany
52
53 Redistribution and use in source and binary forms, with or without
54 modification, are permitted provided that the following conditions
55 are met:
56
57 - Redistributions of source code must retain the above copyright
58 notice, this list of conditions and the following disclaimer.
59
60 - Redistributions in binary form must reproduce the above copyright
61 notice, this list of conditions and the following disclaimer in the
62 documentation and/or other materials provided with the distribution.
63
64 - Neither the name of OFFIS nor the names of its contributors may be
65 used to endorse or promote products derived from this software
66 without specific prior written permission.
67
68 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
69 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
70 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
71 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
72 HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
73 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
74 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
75 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
76 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
77 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
78 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
79
80 =========================================================================*/
81
82
83
84 /***
85
86 Validation:
87
88 # sudo apt-get install dicom3tools
89 # dciodvfy DICOMDIR 2>&1 | less
90 # dcentvfy DICOMDIR 2>&1 | less
91
92 http://www.dclunie.com/dicom3tools/dciodvfy.html
93
94 DICOMDIR viewer working with Wine under Linux:
95 http://www.microdicom.com/
96
97 ***/
98
99
100 #include "PrecompiledHeadersServer.h"
101 #include "DicomDirWriter.h"
102
103 #include "FromDcmtkBridge.h"
104 #include "ToDcmtkBridge.h"
105
106 #include "../Core/OrthancException.h"
107 #include "../Core/Uuid.h"
108
109 #include <dcmtk/dcmdata/dcdicdir.h>
110 #include <dcmtk/dcmdata/dcmetinf.h>
111 #include <dcmtk/dcmdata/dcdeftag.h>
112 #include <dcmtk/dcmdata/dcuid.h>
113 #include <dcmtk/dcmdata/dcddirif.h>
114 #include <dcmtk/dcmdata/dcvrui.h>
115 #include <dcmtk/dcmdata/dcsequen.h>
116 #include <dcmtk/dcmdata/dcostrmf.h>
117 #include "dcmtk/dcmdata/dcvrda.h" /* for class DcmDate */
118 #include "dcmtk/dcmdata/dcvrtm.h" /* for class DcmTime */
119
120 #include <memory>
121 #include <glog/logging.h>
122
123 namespace Orthanc
124 {
125 class DicomDirWriter::PImpl
126 {
127 private:
128 std::string fileSetId_;
129 Toolbox::TemporaryFile file_;
130 std::auto_ptr<DcmDicomDir> dir_;
131
132 typedef std::pair<ResourceType, std::string> IndexKey;
133 typedef std::map<IndexKey, DcmDirectoryRecord* > Index;
134 Index index_;
135
136
137 /*******************************************************************************
138 * Functions adapted from "dcmdata/libsrc/dcddirif.cc" from DCMTK 3.6.0
139 *******************************************************************************/
140
141 // print an error message to the console (stderr) that something went wrong with an attribute
142 static void printAttributeErrorMessage(const DcmTagKey &key,
143 const OFCondition &error,
144 const char *operation)
145 {
146 if (error.bad())
147 {
148 OFString str;
149 if (operation != NULL)
150 {
151 str = "cannot ";
152 str += operation;
153 str += " ";
154 }
155 LOG(ERROR) << error.text() << ": " << str << DcmTag(key).getTagName() << " " << key;
156 }
157 }
158
159 // copy element from dataset to directory record
160 static void copyElement(DcmItem& dataset,
161 const DcmTagKey &key,
162 DcmDirectoryRecord& record,
163 const OFBool optional,
164 const OFBool copyEmpty)
165 {
166 /* check whether tag exists in source dataset (if optional) */
167 if (!optional || (copyEmpty && dataset.tagExists(key)) || dataset.tagExistsWithValue(key))
168 {
169 DcmElement *delem = NULL;
170 /* get copy of element from source dataset */
171 OFCondition status = dataset.findAndGetElement(key, delem, OFFalse /*searchIntoSub*/, OFTrue /*createCopy*/);
172 if (status.good())
173 {
174 /* ... and insert it into the destination dataset (record) */
175 status = record.insert(delem, OFTrue /*replaceOld*/);
176 if (status.good())
177 {
178 DcmTag tag(key);
179 /* check for correct VR in the dataset */
180 if (delem->getVR() != tag.getEVR())
181 {
182 /* create warning message */
183 LOG(WARNING) << "DICOMDIR: possibly wrong VR: "
184 << tag.getTagName() << " " << key << " with "
185 << DcmVR(delem->getVR()).getVRName() << " found, expected "
186 << tag.getVRName() << " instead";
187 }
188 } else
189 delete delem;
190 } else if (status == EC_TagNotFound)
191 status = record.insertEmptyElement(key);
192 printAttributeErrorMessage(key, status, "insert");
193 }
194 }
195
196 // copy optional string value from dataset to directory record
197 static void copyStringWithDefault(DcmItem& dataset,
198 const DcmTagKey &key,
199 DcmDirectoryRecord& record,
200 const char *defaultValue,
201 const OFBool printWarning)
202 {
203 OFCondition status;
204 if (dataset.tagExistsWithValue(key))
205 {
206 OFString stringValue;
207 /* retrieve string value from source dataset and put it into the destination dataset */
208 status = dataset.findAndGetOFStringArray(key, stringValue);
209 if (status.good())
210 status = record.putAndInsertString(key, stringValue.c_str());
211 } else {
212 if (printWarning && (defaultValue != NULL))
213 {
214 /* create warning message */
215 LOG(WARNING) << "DICOMDIR: " << DcmTag(key).getTagName() << " "
216 << key << " missing, using alternative: " << defaultValue;
217 }
218 /* put default value */
219 status = record.putAndInsertString(key, defaultValue);
220 }
221 }
222
223 // create alternative study date if absent in dataset
224 static OFString &alternativeStudyDate(DcmItem& dataset,
225 OFString &result)
226 {
227 /* use another date if present */
228 if (dataset.findAndGetOFStringArray(DCM_SeriesDate, result).bad() || result.empty())
229 {
230 if (dataset.findAndGetOFStringArray(DCM_AcquisitionDate, result).bad() || result.empty())
231 {
232 if (dataset.findAndGetOFStringArray(DCM_ContentDate, result).bad() || result.empty())
233 {
234 /* use current date, "19000101" in case of error */
235 DcmDate::getCurrentDate(result);
236 }
237 }
238 }
239 return result;
240 }
241
242
243 // create alternative study time if absent in dataset
244 static OFString &alternativeStudyTime(DcmItem& dataset,
245 OFString &result)
246 {
247 /* use another time if present */
248 if (dataset.findAndGetOFStringArray(DCM_SeriesTime, result).bad() || result.empty())
249 {
250 if (dataset.findAndGetOFStringArray(DCM_AcquisitionTime, result).bad() || result.empty())
251 {
252 if (dataset.findAndGetOFStringArray(DCM_ContentTime, result).bad() || result.empty())
253 {
254 /* use current time, "0000" in case of error */
255 DcmTime::getCurrentTime(result);
256 }
257 }
258 }
259 return result;
260 }
261
262
263 static void copyElementType1(DcmItem& dataset,
264 const DcmTagKey &key,
265 DcmDirectoryRecord& record)
266 {
267 copyElement(dataset, key, record, OFFalse /*optional*/, OFFalse /*copyEmpty*/);
268 }
269
270 static void copyElementType1C(DcmItem& dataset,
271 const DcmTagKey &key,
272 DcmDirectoryRecord& record)
273 {
274 copyElement(dataset, key, record, OFTrue /*optional*/, OFFalse /*copyEmpty*/);
275 }
276
277 static void copyElementType2(DcmItem& dataset,
278 const DcmTagKey &key,
279 DcmDirectoryRecord& record)
280 {
281 copyElement(dataset, key, record, OFFalse /*optional*/, OFTrue /*copyEmpty*/);
282 }
283
284 /*******************************************************************************
285 * End of functions adapted from "dcmdata/libsrc/dcddirif.cc" from DCMTK 3.6.0
286 *******************************************************************************/
287
288
289 DcmDicomDir& GetDicomDir()
290 {
291 if (dir_.get() == NULL)
292 {
293 dir_.reset(new DcmDicomDir(file_.GetPath().c_str(),
294 fileSetId_.c_str()));
295 }
296
297 return *dir_;
298 }
299
300
301 DcmDirectoryRecord& GetRoot()
302 {
303 return GetDicomDir().getRootRecord();
304 }
305
306
307 public:
308 PImpl() : fileSetId_("ORTHANC_MEDIA")
309 {
310 }
311
312 void FillPatient(DcmDirectoryRecord& record,
313 DcmItem& dicom)
314 {
315 // cf. "DicomDirInterface::buildPatientRecord()"
316
317 copyElementType1C(dicom, DCM_PatientID, record);
318 copyElementType2(dicom, DCM_PatientName, record);
319 }
320
321 void FillStudy(DcmDirectoryRecord& record,
322 DcmItem& dicom)
323 {
324 // cf. "DicomDirInterface::buildStudyRecord()"
325
326 OFString tmpString;
327 /* copy attribute values from dataset to study record */
328 copyStringWithDefault(dicom, DCM_StudyDate, record,
329 alternativeStudyDate(dicom, tmpString).c_str(), OFTrue /*printWarning*/);
330 copyStringWithDefault(dicom, DCM_StudyTime, record,
331 alternativeStudyTime(dicom, tmpString).c_str(), OFTrue /*printWarning*/);
332 copyElementType2(dicom, DCM_StudyDescription, record);
333 copyElementType1(dicom, DCM_StudyInstanceUID, record);
334 /* use type 1C instead of 1 in order to avoid unwanted overwriting */
335 copyElementType1C(dicom, DCM_StudyID, record);
336 copyElementType2(dicom, DCM_AccessionNumber, record);
337 }
338
339 void FillSeries(DcmDirectoryRecord& record,
340 DcmItem& dicom)
341 {
342 // cf. "DicomDirInterface::buildSeriesRecord()"
343
344 /* copy attribute values from dataset to series record */
345 copyElementType1(dicom, DCM_Modality, record);
346 copyElementType1(dicom, DCM_SeriesInstanceUID, record);
347 /* use type 1C instead of 1 in order to avoid unwanted overwriting */
348 copyElementType1C(dicom, DCM_SeriesNumber, record);
349 }
350
351 void FillInstance(DcmDirectoryRecord& record,
352 DcmItem& dicom,
353 DcmMetaInfo& metaInfo,
354 const char* path)
355 {
356 // cf. "DicomDirInterface::buildImageRecord()"
357
358 /* copy attribute values from dataset to image record */
359 copyElementType1(dicom, DCM_InstanceNumber, record);
360 //copyElementType1C(dicom, DCM_ImageType, record);
361 copyElementType1C(dicom, DCM_ReferencedImageSequence, record);
362
363 OFString tmp;
364
365 DcmElement* item = record.remove(DCM_ReferencedImageSequence);
366 if (item != NULL)
367 {
368 delete item;
369 }
370
371 if (record.putAndInsertString(DCM_ReferencedFileID, path).bad() ||
372 dicom.findAndGetOFStringArray(DCM_SOPClassUID, tmp).bad() ||
373 record.putAndInsertString(DCM_ReferencedSOPClassUIDInFile, tmp.c_str()).bad() ||
374 dicom.findAndGetOFStringArray(DCM_SOPInstanceUID, tmp).bad() ||
375 record.putAndInsertString(DCM_ReferencedSOPInstanceUIDInFile, tmp.c_str()).bad() ||
376 metaInfo.findAndGetOFStringArray(DCM_TransferSyntaxUID, tmp).bad() ||
377 record.putAndInsertString(DCM_ReferencedTransferSyntaxUIDInFile, tmp.c_str()).bad())
378 {
379 throw OrthancException(ErrorCode_BadFileFormat);
380 }
381 }
382
383
384
385 bool CreateResource(DcmDirectoryRecord*& target,
386 ResourceType level,
387 DcmFileFormat& dicom,
388 const char* filename,
389 const char* path)
390 {
391 DcmDataset& dataset = *dicom.getDataset();
392
393 OFCondition result;
394 OFString id;
395 E_DirRecType type;
396
397 switch (level)
398 {
399 case ResourceType_Patient:
400 result = dataset.findAndGetOFString(DCM_PatientID, id);
401 type = ERT_Patient;
402 break;
403
404 case ResourceType_Study:
405 result = dataset.findAndGetOFString(DCM_StudyInstanceUID, id);
406 type = ERT_Study;
407 break;
408
409 case ResourceType_Series:
410 result = dataset.findAndGetOFString(DCM_SeriesInstanceUID, id);
411 type = ERT_Series;
412 break;
413
414 case ResourceType_Instance:
415 result = dataset.findAndGetOFString(DCM_SOPInstanceUID, id);
416 type = ERT_Image;
417 break;
418
419 default:
420 throw OrthancException(ErrorCode_InternalError);
421 }
422
423 if (!result.good())
424 {
425 throw OrthancException(ErrorCode_InternalError);
426 }
427
428 IndexKey key = std::make_pair(level, std::string(id.c_str()));
429 Index::iterator it = index_.find(key);
430
431 if (it != index_.end())
432 {
433 target = it->second;
434 return false; // Already existing
435 }
436
437 std::auto_ptr<DcmDirectoryRecord> record(new DcmDirectoryRecord(type, NULL, filename));
438
439 switch (level)
440 {
441 case ResourceType_Patient:
442 FillPatient(*record, dataset);
443 break;
444
445 case ResourceType_Study:
446 FillStudy(*record, dataset);
447 break;
448
449 case ResourceType_Series:
450 FillSeries(*record, dataset);
451 break;
452
453 case ResourceType_Instance:
454 FillInstance(*record, dataset, *dicom.getMetaInfo(), path);
455 break;
456
457 default:
458 throw OrthancException(ErrorCode_InternalError);
459 }
460
461 if (record->isAffectedBySpecificCharacterSet())
462 {
463 copyElementType1C(dataset, DCM_SpecificCharacterSet, *record);
464 }
465
466 target = record.get();
467 GetRoot().insertSub(record.release());
468 index_[key] = target;
469
470 return true; // Newly created
471 }
472
473 void Read(std::string& s)
474 {
475 if (!GetDicomDir().write(DICOMDIR_DEFAULT_TRANSFERSYNTAX,
476 EET_UndefinedLength /*encodingType*/,
477 EGL_withoutGL /*groupLength*/).good())
478 {
479 throw OrthancException(ErrorCode_InternalError);
480 }
481
482 file_.Read(s);
483 }
484
485 void SetFileSetId(const std::string& id)
486 {
487 dir_.reset(NULL);
488 fileSetId_ = id;
489 }
490 };
491
492
493 DicomDirWriter::DicomDirWriter() : pimpl_(new PImpl)
494 {
495 }
496
497 DicomDirWriter::~DicomDirWriter()
498 {
499 if (pimpl_)
500 {
501 delete pimpl_;
502 }
503 }
504
505 void DicomDirWriter::SetFileSetId(const std::string& id)
506 {
507 pimpl_->SetFileSetId(id);
508 }
509
510 void DicomDirWriter::Add(const std::string& directory,
511 const std::string& filename,
512 ParsedDicomFile& dicom)
513 {
514 std::string path;
515 if (directory.empty())
516 {
517 path = filename;
518 }
519 else
520 {
521 if (directory[directory.length() - 1] == '/' ||
522 directory[directory.length() - 1] == '\\')
523 {
524 throw OrthancException(ErrorCode_ParameterOutOfRange);
525 }
526
527 path = directory + '\\' + filename;
528 }
529
530 DcmFileFormat& fileFormat = *reinterpret_cast<DcmFileFormat*>(dicom.GetDcmtkObject());
531
532 DcmDirectoryRecord* instance;
533 bool isNewInstance = pimpl_->CreateResource(instance, ResourceType_Instance, fileFormat, filename.c_str(), path.c_str());
534 if (isNewInstance)
535 {
536 DcmDirectoryRecord* series;
537 bool isNewSeries = pimpl_->CreateResource(series, ResourceType_Series, fileFormat, filename.c_str(), NULL);
538 series->insertSub(instance);
539
540 if (isNewSeries)
541 {
542 DcmDirectoryRecord* study;
543 bool isNewStudy = pimpl_->CreateResource(study, ResourceType_Study, fileFormat, filename.c_str(), NULL);
544 study->insertSub(series);
545
546 if (isNewStudy)
547 {
548 DcmDirectoryRecord* patient;
549 pimpl_->CreateResource(patient, ResourceType_Patient, fileFormat, filename.c_str(), NULL);
550 patient->insertSub(study);
551 }
552 }
553 }
554
555
556 {
557 // DEBUG
558 static unsigned int count = 0;
559 char buf[1024];
560 sprintf(buf, "/tmp/dicomdir-%06d.dcm", count++);
561
562 std::string s;
563 pimpl_->Read(s);
564 Toolbox::WriteFile(s, buf);
565 }
566 }
567
568 void DicomDirWriter::Encode(std::string& target)
569 {
570 pimpl_->Read(target);
571 }
572 }