# HG changeset patch # User Sebastien Jodogne # Date 1409920123 -7200 # Node ID 82567bac5e253caeaa681d3221afa7f62a5020f1 # Parent 009dce4ea2f65e09aa9f51a1662d61a4e7baa164 Creation of ZIP archives for media storage, with DICOMDIR diff -r 009dce4ea2f6 -r 82567bac5e25 CMakeLists.txt --- a/CMakeLists.txt Wed Sep 03 16:49:26 2014 +0200 +++ b/CMakeLists.txt Fri Sep 05 14:28:43 2014 +0200 @@ -143,6 +143,7 @@ OrthancServer/DicomModification.cpp OrthancServer/FromDcmtkBridge.cpp OrthancServer/ParsedDicomFile.cpp + OrthancServer/DicomDirWriter.cpp OrthancServer/Internals/CommandDispatcher.cpp OrthancServer/Internals/FindScp.cpp OrthancServer/Internals/MoveScp.cpp diff -r 009dce4ea2f6 -r 82567bac5e25 Core/Compression/HierarchicalZipWriter.h --- a/Core/Compression/HierarchicalZipWriter.h Wed Sep 03 16:49:26 2014 +0200 +++ b/Core/Compression/HierarchicalZipWriter.h Fri Sep 05 14:28:43 2014 +0200 @@ -63,8 +63,6 @@ Stack stack_; - std::string GetCurrentDirectoryPath() const; - std::string EnsureUniqueFilename(const char* filename); public: @@ -83,6 +81,8 @@ void CloseDirectory(); + std::string GetCurrentDirectoryPath() const; + static std::string KeepAlphanumeric(const std::string& source); }; @@ -120,6 +120,11 @@ void CloseDirectory(); + std::string GetCurrentDirectoryPath() const + { + return indexer_.GetCurrentDirectoryPath(); + } + void Write(const char* data, size_t length) { writer_.Write(data, length); diff -r 009dce4ea2f6 -r 82567bac5e25 NEWS --- a/NEWS Wed Sep 03 16:49:26 2014 +0200 +++ b/NEWS Fri Sep 05 14:28:43 2014 +0200 @@ -1,6 +1,7 @@ Pending changes in the mainline =============================== +* Creation of ZIP archives for media storage, with DICOMDIR * Refactoring of HttpOutput ("Content-Length" header is now always sent) * "/tools/create-dicom" now accepts the "PatientID" DICOM tag (+ updated sample) * Fixes for Visual Studio 2013 and Windows 64bit diff -r 009dce4ea2f6 -r 82567bac5e25 OrthancServer/DicomDirWriter.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DicomDirWriter.cpp Fri Sep 05 14:28:43 2014 +0200 @@ -0,0 +1,572 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + + + + +/*========================================================================= + + This file is based on portions of the following project: + + Program: DCMTK 3.6.0 + Module: http://dicom.offis.de/dcmtk.php.en + +Copyright (C) 1994-2011, OFFIS e.V. +All rights reserved. + +This software and supporting documentation were developed by + + OFFIS e.V. + R&D Division Health + Escherweg 2 + 26121 Oldenburg, Germany + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +- Neither the name of OFFIS nor the names of its contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=========================================================================*/ + + + +/*** + + Validation: + + # sudo apt-get install dicom3tools + # dciodvfy DICOMDIR 2>&1 | less + # dcentvfy DICOMDIR 2>&1 | less + + http://www.dclunie.com/dicom3tools/dciodvfy.html + + DICOMDIR viewer working with Wine under Linux: + http://www.microdicom.com/ + + ***/ + + +#include "PrecompiledHeadersServer.h" +#include "DicomDirWriter.h" + +#include "FromDcmtkBridge.h" +#include "ToDcmtkBridge.h" + +#include "../Core/OrthancException.h" +#include "../Core/Uuid.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include "dcmtk/dcmdata/dcvrda.h" /* for class DcmDate */ +#include "dcmtk/dcmdata/dcvrtm.h" /* for class DcmTime */ + +#include +#include + +namespace Orthanc +{ + class DicomDirWriter::PImpl + { + private: + std::string fileSetId_; + Toolbox::TemporaryFile file_; + std::auto_ptr dir_; + + typedef std::pair IndexKey; + typedef std::map Index; + Index index_; + + + /******************************************************************************* + * Functions adapted from "dcmdata/libsrc/dcddirif.cc" from DCMTK 3.6.0 + *******************************************************************************/ + + // print an error message to the console (stderr) that something went wrong with an attribute + static void printAttributeErrorMessage(const DcmTagKey &key, + const OFCondition &error, + const char *operation) + { + if (error.bad()) + { + OFString str; + if (operation != NULL) + { + str = "cannot "; + str += operation; + str += " "; + } + LOG(ERROR) << error.text() << ": " << str << DcmTag(key).getTagName() << " " << key; + } + } + + // copy element from dataset to directory record + static void copyElement(DcmItem& dataset, + const DcmTagKey &key, + DcmDirectoryRecord& record, + const OFBool optional, + const OFBool copyEmpty) + { + /* check whether tag exists in source dataset (if optional) */ + if (!optional || (copyEmpty && dataset.tagExists(key)) || dataset.tagExistsWithValue(key)) + { + DcmElement *delem = NULL; + /* get copy of element from source dataset */ + OFCondition status = dataset.findAndGetElement(key, delem, OFFalse /*searchIntoSub*/, OFTrue /*createCopy*/); + if (status.good()) + { + /* ... and insert it into the destination dataset (record) */ + status = record.insert(delem, OFTrue /*replaceOld*/); + if (status.good()) + { + DcmTag tag(key); + /* check for correct VR in the dataset */ + if (delem->getVR() != tag.getEVR()) + { + /* create warning message */ + LOG(WARNING) << "DICOMDIR: possibly wrong VR: " + << tag.getTagName() << " " << key << " with " + << DcmVR(delem->getVR()).getVRName() << " found, expected " + << tag.getVRName() << " instead"; + } + } else + delete delem; + } else if (status == EC_TagNotFound) + status = record.insertEmptyElement(key); + printAttributeErrorMessage(key, status, "insert"); + } + } + + // copy optional string value from dataset to directory record + static void copyStringWithDefault(DcmItem& dataset, + const DcmTagKey &key, + DcmDirectoryRecord& record, + const char *defaultValue, + const OFBool printWarning) + { + OFCondition status; + if (dataset.tagExistsWithValue(key)) + { + OFString stringValue; + /* retrieve string value from source dataset and put it into the destination dataset */ + status = dataset.findAndGetOFStringArray(key, stringValue); + if (status.good()) + status = record.putAndInsertString(key, stringValue.c_str()); + } else { + if (printWarning && (defaultValue != NULL)) + { + /* create warning message */ + LOG(WARNING) << "DICOMDIR: " << DcmTag(key).getTagName() << " " + << key << " missing, using alternative: " << defaultValue; + } + /* put default value */ + status = record.putAndInsertString(key, defaultValue); + } + } + + // create alternative study date if absent in dataset + static OFString &alternativeStudyDate(DcmItem& dataset, + OFString &result) + { + /* use another date if present */ + if (dataset.findAndGetOFStringArray(DCM_SeriesDate, result).bad() || result.empty()) + { + if (dataset.findAndGetOFStringArray(DCM_AcquisitionDate, result).bad() || result.empty()) + { + if (dataset.findAndGetOFStringArray(DCM_ContentDate, result).bad() || result.empty()) + { + /* use current date, "19000101" in case of error */ + DcmDate::getCurrentDate(result); + } + } + } + return result; + } + + + // create alternative study time if absent in dataset + static OFString &alternativeStudyTime(DcmItem& dataset, + OFString &result) + { + /* use another time if present */ + if (dataset.findAndGetOFStringArray(DCM_SeriesTime, result).bad() || result.empty()) + { + if (dataset.findAndGetOFStringArray(DCM_AcquisitionTime, result).bad() || result.empty()) + { + if (dataset.findAndGetOFStringArray(DCM_ContentTime, result).bad() || result.empty()) + { + /* use current time, "0000" in case of error */ + DcmTime::getCurrentTime(result); + } + } + } + return result; + } + + + static void copyElementType1(DcmItem& dataset, + const DcmTagKey &key, + DcmDirectoryRecord& record) + { + copyElement(dataset, key, record, OFFalse /*optional*/, OFFalse /*copyEmpty*/); + } + + static void copyElementType1C(DcmItem& dataset, + const DcmTagKey &key, + DcmDirectoryRecord& record) + { + copyElement(dataset, key, record, OFTrue /*optional*/, OFFalse /*copyEmpty*/); + } + + static void copyElementType2(DcmItem& dataset, + const DcmTagKey &key, + DcmDirectoryRecord& record) + { + copyElement(dataset, key, record, OFFalse /*optional*/, OFTrue /*copyEmpty*/); + } + + /******************************************************************************* + * End of functions adapted from "dcmdata/libsrc/dcddirif.cc" from DCMTK 3.6.0 + *******************************************************************************/ + + + DcmDicomDir& GetDicomDir() + { + if (dir_.get() == NULL) + { + dir_.reset(new DcmDicomDir(file_.GetPath().c_str(), + fileSetId_.c_str())); + } + + return *dir_; + } + + + DcmDirectoryRecord& GetRoot() + { + return GetDicomDir().getRootRecord(); + } + + + public: + PImpl() : fileSetId_("ORTHANC_MEDIA") + { + } + + void FillPatient(DcmDirectoryRecord& record, + DcmItem& dicom) + { + // cf. "DicomDirInterface::buildPatientRecord()" + + copyElementType1C(dicom, DCM_PatientID, record); + copyElementType2(dicom, DCM_PatientName, record); + } + + void FillStudy(DcmDirectoryRecord& record, + DcmItem& dicom) + { + // cf. "DicomDirInterface::buildStudyRecord()" + + OFString tmpString; + /* copy attribute values from dataset to study record */ + copyStringWithDefault(dicom, DCM_StudyDate, record, + alternativeStudyDate(dicom, tmpString).c_str(), OFTrue /*printWarning*/); + copyStringWithDefault(dicom, DCM_StudyTime, record, + alternativeStudyTime(dicom, tmpString).c_str(), OFTrue /*printWarning*/); + copyElementType2(dicom, DCM_StudyDescription, record); + copyElementType1(dicom, DCM_StudyInstanceUID, record); + /* use type 1C instead of 1 in order to avoid unwanted overwriting */ + copyElementType1C(dicom, DCM_StudyID, record); + copyElementType2(dicom, DCM_AccessionNumber, record); + } + + void FillSeries(DcmDirectoryRecord& record, + DcmItem& dicom) + { + // cf. "DicomDirInterface::buildSeriesRecord()" + + /* copy attribute values from dataset to series record */ + copyElementType1(dicom, DCM_Modality, record); + copyElementType1(dicom, DCM_SeriesInstanceUID, record); + /* use type 1C instead of 1 in order to avoid unwanted overwriting */ + copyElementType1C(dicom, DCM_SeriesNumber, record); + } + + void FillInstance(DcmDirectoryRecord& record, + DcmItem& dicom, + DcmMetaInfo& metaInfo, + const char* path) + { + // cf. "DicomDirInterface::buildImageRecord()" + + /* copy attribute values from dataset to image record */ + copyElementType1(dicom, DCM_InstanceNumber, record); + //copyElementType1C(dicom, DCM_ImageType, record); + copyElementType1C(dicom, DCM_ReferencedImageSequence, record); + + OFString tmp; + + DcmElement* item = record.remove(DCM_ReferencedImageSequence); + if (item != NULL) + { + delete item; + } + + if (record.putAndInsertString(DCM_ReferencedFileID, path).bad() || + dicom.findAndGetOFStringArray(DCM_SOPClassUID, tmp).bad() || + record.putAndInsertString(DCM_ReferencedSOPClassUIDInFile, tmp.c_str()).bad() || + dicom.findAndGetOFStringArray(DCM_SOPInstanceUID, tmp).bad() || + record.putAndInsertString(DCM_ReferencedSOPInstanceUIDInFile, tmp.c_str()).bad() || + metaInfo.findAndGetOFStringArray(DCM_TransferSyntaxUID, tmp).bad() || + record.putAndInsertString(DCM_ReferencedTransferSyntaxUIDInFile, tmp.c_str()).bad()) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + + + bool CreateResource(DcmDirectoryRecord*& target, + ResourceType level, + DcmFileFormat& dicom, + const char* filename, + const char* path) + { + DcmDataset& dataset = *dicom.getDataset(); + + OFCondition result; + OFString id; + E_DirRecType type; + + switch (level) + { + case ResourceType_Patient: + result = dataset.findAndGetOFString(DCM_PatientID, id); + type = ERT_Patient; + break; + + case ResourceType_Study: + result = dataset.findAndGetOFString(DCM_StudyInstanceUID, id); + type = ERT_Study; + break; + + case ResourceType_Series: + result = dataset.findAndGetOFString(DCM_SeriesInstanceUID, id); + type = ERT_Series; + break; + + case ResourceType_Instance: + result = dataset.findAndGetOFString(DCM_SOPInstanceUID, id); + type = ERT_Image; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + if (!result.good()) + { + throw OrthancException(ErrorCode_InternalError); + } + + IndexKey key = std::make_pair(level, std::string(id.c_str())); + Index::iterator it = index_.find(key); + + if (it != index_.end()) + { + target = it->second; + return false; // Already existing + } + + std::auto_ptr record(new DcmDirectoryRecord(type, NULL, filename)); + + switch (level) + { + case ResourceType_Patient: + FillPatient(*record, dataset); + break; + + case ResourceType_Study: + FillStudy(*record, dataset); + break; + + case ResourceType_Series: + FillSeries(*record, dataset); + break; + + case ResourceType_Instance: + FillInstance(*record, dataset, *dicom.getMetaInfo(), path); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + if (record->isAffectedBySpecificCharacterSet()) + { + copyElementType1C(dataset, DCM_SpecificCharacterSet, *record); + } + + target = record.get(); + GetRoot().insertSub(record.release()); + index_[key] = target; + + return true; // Newly created + } + + void Read(std::string& s) + { + if (!GetDicomDir().write(DICOMDIR_DEFAULT_TRANSFERSYNTAX, + EET_UndefinedLength /*encodingType*/, + EGL_withoutGL /*groupLength*/).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + + file_.Read(s); + } + + void SetFileSetId(const std::string& id) + { + dir_.reset(NULL); + fileSetId_ = id; + } + }; + + + DicomDirWriter::DicomDirWriter() : pimpl_(new PImpl) + { + } + + DicomDirWriter::~DicomDirWriter() + { + if (pimpl_) + { + delete pimpl_; + } + } + + void DicomDirWriter::SetFileSetId(const std::string& id) + { + pimpl_->SetFileSetId(id); + } + + void DicomDirWriter::Add(const std::string& directory, + const std::string& filename, + ParsedDicomFile& dicom) + { + std::string path; + if (directory.empty()) + { + path = filename; + } + else + { + if (directory[directory.length() - 1] == '/' || + directory[directory.length() - 1] == '\\') + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + path = directory + '\\' + filename; + } + + DcmFileFormat& fileFormat = *reinterpret_cast(dicom.GetDcmtkObject()); + + DcmDirectoryRecord* instance; + bool isNewInstance = pimpl_->CreateResource(instance, ResourceType_Instance, fileFormat, filename.c_str(), path.c_str()); + if (isNewInstance) + { + DcmDirectoryRecord* series; + bool isNewSeries = pimpl_->CreateResource(series, ResourceType_Series, fileFormat, filename.c_str(), NULL); + series->insertSub(instance); + + if (isNewSeries) + { + DcmDirectoryRecord* study; + bool isNewStudy = pimpl_->CreateResource(study, ResourceType_Study, fileFormat, filename.c_str(), NULL); + study->insertSub(series); + + if (isNewStudy) + { + DcmDirectoryRecord* patient; + pimpl_->CreateResource(patient, ResourceType_Patient, fileFormat, filename.c_str(), NULL); + patient->insertSub(study); + } + } + } + + + { + // DEBUG + static unsigned int count = 0; + char buf[1024]; + sprintf(buf, "/tmp/dicomdir-%06d.dcm", count++); + + std::string s; + pimpl_->Read(s); + Toolbox::WriteFile(s, buf); + } + } + + void DicomDirWriter::Encode(std::string& target) + { + pimpl_->Read(target); + } +} diff -r 009dce4ea2f6 -r 82567bac5e25 OrthancServer/DicomDirWriter.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DicomDirWriter.h Fri Sep 05 14:28:43 2014 +0200 @@ -0,0 +1,59 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "ParsedDicomFile.h" + +namespace Orthanc +{ + class DicomDirWriter + { + private: + class PImpl; + PImpl* pimpl_; + + public: + DicomDirWriter(); + + ~DicomDirWriter(); + + void SetFileSetId(const std::string& id); + + void Add(const std::string& directory, + const std::string& filename, + ParsedDicomFile& dicom); + + void Encode(std::string& target); + }; + +} diff -r 009dce4ea2f6 -r 82567bac5e25 OrthancServer/OrthancRestApi/OrthancRestArchive.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Wed Sep 03 16:49:26 2014 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Fri Sep 05 14:28:43 2014 +0200 @@ -33,6 +33,7 @@ #include "../PrecompiledHeadersServer.h" #include "OrthancRestApi.h" +#include "../DicomDirWriter.h" #include "../../Core/Compression/HierarchicalZipWriter.h" #include "../../Core/HttpServer/FilesystemHttpSender.h" #include "../../Core/Uuid.h" @@ -54,30 +55,38 @@ static std::string GetDirectoryNameInArchive(const Json::Value& resource, ResourceType resourceType) { + std::string s; + switch (resourceType) { case ResourceType_Patient: { std::string p = resource["MainDicomTags"]["PatientID"].asString(); std::string n = resource["MainDicomTags"]["PatientName"].asString(); - return p + " " + n; + s = p + " " + n; + break; } case ResourceType_Study: { - return resource["MainDicomTags"]["StudyDescription"].asString(); + s = resource["MainDicomTags"]["StudyDescription"].asString(); + break; } case ResourceType_Series: { std::string d = resource["MainDicomTags"]["SeriesDescription"].asString(); std::string m = resource["MainDicomTags"]["Modality"].asString(); - return m + " " + d; + s = m + " " + d; + break; } default: throw OrthancException(ErrorCode_InternalError); } + + // Get rid of special characters + return Toolbox::ConvertToAscii(s); } static bool CreateRootDirectoryInArchive(HierarchicalZipWriter& writer, @@ -126,13 +135,6 @@ const std::string& instancePublicId, const char* filename) { - Json::Value instance; - - if (!context.GetIndex().LookupResource(instance, instancePublicId, ResourceType_Instance)) - { - return false; - } - writer.OpenFile(filename); std::string dicom; @@ -233,13 +235,10 @@ return true; } - template - static void GetArchive(RestApiGetCall& call) + + static bool IsZip64Required(ServerIndex& index, + const std::string& id) { - ServerContext& context = OrthancRestApi::GetContext(call); - - std::string id = call.GetUriComponent("id", ""); - /** * Determine whether ZIP64 is required. Original ZIP format can * store up to 2GB of data (some implementation supporting up to @@ -252,8 +251,8 @@ unsigned int countStudies; unsigned int countSeries; unsigned int countInstances; - context.GetIndex().GetStatistics(compressedSize, uncompressedSize, - countStudies, countSeries, countInstances, id); + index.GetStatistics(compressedSize, uncompressedSize, + countStudies, countSeries, countInstances, id); const bool isZip64 = (uncompressedSize >= 2 * GIGA_BYTES || countInstances >= 65535); @@ -261,6 +260,18 @@ << (uncompressedSize / MEGA_BYTES) << "MB using the " << (isZip64 ? "ZIP64" : "ZIP32") << " file format"; + return isZip64; + } + + + template + static void GetArchive(RestApiGetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string id = call.GetUriComponent("id", ""); + bool isZip64 = IsZip64Required(context.GetIndex(), id); + // Create a RAII for the temporary file to manage the ZIP file Toolbox::TemporaryFile tmp; @@ -288,10 +299,75 @@ } + static void GetMediaArchive(RestApiGetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string id = call.GetUriComponent("id", ""); + bool isZip64 = IsZip64Required(context.GetIndex(), id); + + // Create a RAII for the temporary file to manage the ZIP file + Toolbox::TemporaryFile tmp; + + { + // Create a ZIP writer + HierarchicalZipWriter writer(tmp.GetPath().c_str()); + writer.SetZip64(isZip64); + writer.OpenDirectory("IMAGES"); + + // Create the DICOMDIR writer + DicomDirWriter dicomDir; + + // Retrieve the list of the instances + std::list instances; + context.GetIndex().GetChildInstances(instances, id); + + size_t pos = 0; + for (std::list::const_iterator + it = instances.begin(); it != instances.end(); it++, pos++) + { + // "DICOM restricts the filenames on DICOM media to 8 + // characters (some systems wrongly use 8.3, but this does not + // conform to the standard)." + std::string filename = "IM" + boost::lexical_cast(pos); + writer.OpenFile(filename.c_str()); + + std::string dicom; + context.ReadFile(dicom, *it, FileContentType_Dicom); + writer.Write(dicom); + + ParsedDicomFile parsed(dicom); + dicomDir.Add("IMAGES", filename, parsed); + } + + // Add the DICOMDIR + writer.CloseDirectory(); + writer.OpenFile("DICOMDIR"); + std::string s; + dicomDir.Encode(s); + writer.Write(s); + } + + // Prepare the sending of the ZIP file + FilesystemHttpSender sender(tmp.GetPath().c_str()); + sender.SetContentType("application/zip"); + sender.SetDownloadFilename(id + ".zip"); + + // Send the ZIP + call.GetOutput().AnswerFile(sender); + + // The temporary file is automatically removed thanks to the RAII + } + + void OrthancRestApi::RegisterArchive() { Register("/patients/{id}/archive", GetArchive); Register("/studies/{id}/archive", GetArchive); Register("/series/{id}/archive", GetArchive); + + Register("/patients/{id}/media", GetMediaArchive); + Register("/studies/{id}/media", GetMediaArchive); + Register("/series/{id}/media", GetMediaArchive); } }