changeset 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 009dce4ea2f6
children 1d60316c3618
files CMakeLists.txt Core/Compression/HierarchicalZipWriter.h NEWS OrthancServer/DicomDirWriter.cpp OrthancServer/DicomDirWriter.h OrthancServer/OrthancRestApi/OrthancRestArchive.cpp
diffstat 6 files changed, 734 insertions(+), 20 deletions(-) [+]
line wrap: on
line diff
--- 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
--- 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);
--- 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
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+
+
+
+/*=========================================================================
+
+  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 <dcmtk/dcmdata/dcdicdir.h>
+#include <dcmtk/dcmdata/dcmetinf.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <dcmtk/dcmdata/dcuid.h>
+#include <dcmtk/dcmdata/dcddirif.h>
+#include <dcmtk/dcmdata/dcvrui.h>
+#include <dcmtk/dcmdata/dcsequen.h>
+#include <dcmtk/dcmdata/dcostrmf.h>
+#include "dcmtk/dcmdata/dcvrda.h"     /* for class DcmDate */
+#include "dcmtk/dcmdata/dcvrtm.h"     /* for class DcmTime */
+
+#include <memory>
+#include <glog/logging.h>
+
+namespace Orthanc
+{
+  class DicomDirWriter::PImpl
+  {
+  private:
+    std::string fileSetId_;
+    Toolbox::TemporaryFile file_;
+    std::auto_ptr<DcmDicomDir> dir_;
+
+    typedef std::pair<ResourceType, std::string>  IndexKey;
+    typedef std::map<IndexKey, DcmDirectoryRecord* >  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<DcmDirectoryRecord> 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<DcmFileFormat*>(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);
+  }
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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);
+  };
+
+}
--- 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 <enum ResourceType resourceType>
-  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 <enum ResourceType resourceType>
+  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<std::string> instances;
+      context.GetIndex().GetChildInstances(instances, id);
+
+      size_t pos = 0;
+      for (std::list<std::string>::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<std::string>(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<ResourceType_Patient>);
     Register("/studies/{id}/archive", GetArchive<ResourceType_Study>);
     Register("/series/{id}/archive", GetArchive<ResourceType_Series>);
+
+    Register("/patients/{id}/media", GetMediaArchive);
+    Register("/studies/{id}/media", GetMediaArchive);
+    Register("/series/{id}/media", GetMediaArchive);
   }
 }