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