changeset 2708:83c991aeb611 jobs

integration mainline->jobs
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 09 Jul 2018 08:18:22 +0200
parents 33de4a82c466 (current diff) 38a3054b22ff (diff)
children ef97db3760ed
files NEWS OrthancServer/main.cpp Resources/CMake/OrthancFrameworkConfiguration.cmake
diffstat 18 files changed, 748 insertions(+), 18 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Thu Jul 05 17:10:10 2018 +0200
+++ b/.hgignore	Mon Jul 09 08:18:22 2018 +0200
@@ -1,2 +1,5 @@
 syntax: glob
-ThirdPartyDownloads/
\ No newline at end of file
+ThirdPartyDownloads/
+CMakeLists.txt.user
+*.cpp.orig
+*.h.orig
--- a/Core/DicomParsing/Internals/DicomImageDecoder.cpp	Thu Jul 05 17:10:10 2018 +0200
+++ b/Core/DicomParsing/Internals/DicomImageDecoder.cpp	Mon Jul 09 08:18:22 2018 +0200
@@ -93,6 +93,7 @@
 #if ORTHANC_ENABLE_JPEG == 1
 #  include "../../Images/JpegWriter.h"
 #endif
+#include "../../Images/PamWriter.h"
 
 #include <boost/lexical_cast.hpp>
 
@@ -952,6 +953,17 @@
   }
 
 
+  void DicomImageDecoder::ExtractPamImage(std::string& result,
+                                          std::auto_ptr<ImageAccessor>& image,
+                                          ImageExtractionMode mode,
+                                          bool invert)
+  {
+    ApplyExtractionMode(image, mode, invert);
+
+    PamWriter writer;
+    writer.WriteToMemory(result, *image);
+  }
+
 #if ORTHANC_ENABLE_PNG == 1
   void DicomImageDecoder::ExtractPngImage(std::string& result,
                                           std::auto_ptr<ImageAccessor>& image,
--- a/Core/DicomParsing/Internals/DicomImageDecoder.h	Thu Jul 05 17:10:10 2018 +0200
+++ b/Core/DicomParsing/Internals/DicomImageDecoder.h	Mon Jul 09 08:18:22 2018 +0200
@@ -101,6 +101,11 @@
     static ImageAccessor *Decode(ParsedDicomFile& dicom,
                                  unsigned int frame);
 
+    static void ExtractPamImage(std::string& result,
+                                std::auto_ptr<ImageAccessor>& image,
+                                ImageExtractionMode mode,
+                                bool invert);
+
 #if ORTHANC_ENABLE_PNG == 1
     static void ExtractPngImage(std::string& result,
                                 std::auto_ptr<ImageAccessor>& image,
--- a/Core/Endianness.h	Thu Jul 05 17:10:10 2018 +0200
+++ b/Core/Endianness.h	Mon Jul 09 08:18:22 2018 +0200
@@ -145,7 +145,13 @@
 
 static inline uint16_t __orthanc_bswap16(uint16_t a)
 {
-  return (a << 8) | (a >> 8);
+  const uint8_t* p = reinterpret_cast<const uint8_t*>(&a);
+  return (static_cast<uint16_t>(p[0]) << 8 |
+          static_cast<uint16_t>(p[1]));
+
+  // WARNING: The implementation below makes LSB (Linux Standard
+  // Base) segfault in release builds. Don't use it!!!
+  // return (a << 8) | (a >> 8);
 }
 
 static inline uint32_t __orthanc_bswap32(uint32_t a)
--- a/Core/EnumerationDictionary.h	Thu Jul 05 17:10:10 2018 +0200
+++ b/Core/EnumerationDictionary.h	Mon Jul 09 08:18:22 2018 +0200
@@ -84,14 +84,11 @@
 
       Enumeration Translate(const std::string& str) const
       {
-        try
+        int value;
+        if (boost::conversion::try_lexical_convert<int>(str, value))
         {
-          int value = boost::lexical_cast<int>(str);
           return static_cast<Enumeration>(value);
         }
-        catch (boost::bad_lexical_cast)
-        {
-        }
 
         typename StringToEnumeration::const_iterator
           found = stringToEnumeration_.find(str);
--- a/Core/Images/JpegWriter.cpp	Thu Jul 05 17:10:10 2018 +0200
+++ b/Core/Images/JpegWriter.cpp	Mon Jul 09 08:18:22 2018 +0200
@@ -166,7 +166,6 @@
 #endif
 
 
-#if ORTHANC_SANDBOXED == 0
   void JpegWriter::WriteToMemoryInternal(std::string& jpeg,
                                          unsigned int width,
                                          unsigned int height,
@@ -211,5 +210,4 @@
     jpeg.assign(reinterpret_cast<const char*>(data), size);
     free(data);
   }
-#endif
 }
--- a/Core/Images/JpegWriter.h	Thu Jul 05 17:10:10 2018 +0200
+++ b/Core/Images/JpegWriter.h	Mon Jul 09 08:18:22 2018 +0200
@@ -48,21 +48,21 @@
   class JpegWriter : public IImageWriter
   {
   protected:
+#if ORTHANC_SANDBOXED == 0
     virtual void WriteToFileInternal(const std::string& filename,
                                      unsigned int width,
                                      unsigned int height,
                                      unsigned int pitch,
                                      PixelFormat format,
                                      const void* buffer);
+#endif
 
-#if ORTHANC_SANDBOXED == 0
     virtual void WriteToMemoryInternal(std::string& jpeg,
                                        unsigned int width,
                                        unsigned int height,
                                        unsigned int pitch,
                                        PixelFormat format,
                                        const void* buffer);
-#endif
 
   private:
     uint8_t  quality_;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Images/PamReader.cpp	Mon Jul 09 08:18:22 2018 +0200
@@ -0,0 +1,241 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., 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/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "PamReader.h"
+
+#include "../Endianness.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+#if ORTHANC_SANDBOXED == 0
+#  include "../SystemToolbox.h"
+#endif
+
+#include <boost/algorithm/string/find.hpp>
+#include <boost/lexical_cast.hpp>
+
+
+namespace Orthanc
+{
+  static void GetPixelFormat(PixelFormat& format,
+                             unsigned int& bytesPerChannel,
+                             const unsigned int& maxValue,
+                             const unsigned int& channelCount,
+                             const std::string& tupleType)
+  {
+    if (tupleType == "GRAYSCALE" &&
+        channelCount == 1)
+    {
+      switch (maxValue)
+      {
+        case 255:
+          format = PixelFormat_Grayscale8;
+          bytesPerChannel = 1;
+          return;
+
+        case 65535:
+          format = PixelFormat_Grayscale16;
+          bytesPerChannel = 2;
+          return;
+
+        default:
+          throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+    else if (tupleType == "RGB" &&
+             channelCount == 3)
+    {
+      switch (maxValue)
+      {
+        case 255:
+          format = PixelFormat_RGB24;
+          bytesPerChannel = 1;
+          return;
+
+        case 65535:
+          format = PixelFormat_RGB48;
+          bytesPerChannel = 2;
+          return;
+
+        default:
+          throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+  
+  typedef std::map<std::string, std::string>  Parameters;
+
+  
+  static std::string LookupStringParameter(const Parameters& parameters,
+                                           const std::string& key)
+  {
+    Parameters::const_iterator found = parameters.find(key);
+
+    if (found == parameters.end())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+    else
+    {
+      return found->second;
+    }
+  }
+  
+
+  static unsigned int LookupIntegerParameter(const Parameters& parameters,
+                                             const std::string& key)
+  {
+    try
+    {
+      int value = boost::lexical_cast<int>(LookupStringParameter(parameters, key));
+
+      if (value < 0)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        return static_cast<unsigned int>(value);
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+  
+
+  void PamReader::ParseContent()
+  {
+    static const std::string headerDelimiter = "ENDHDR\n";
+    
+    boost::iterator_range<std::string::const_iterator> headerRange =
+      boost::algorithm::find_first(content_, headerDelimiter);
+
+    if (!headerRange)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    std::string header(static_cast<const std::string&>(content_).begin(), headerRange.begin());
+
+    std::vector<std::string> lines;
+    Toolbox::TokenizeString(lines, header, '\n');
+
+    if (lines.size() < 2 ||
+        lines.front() != "P7" ||
+        !lines.back().empty())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    Parameters parameters;
+    
+    for (size_t i = 1; i + 1 < lines.size(); i++)
+    {
+      std::vector<std::string> tokens;
+      Toolbox::TokenizeString(tokens, lines[i], ' ');
+
+      if (tokens.size() != 2)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        parameters[tokens[0]] = tokens[1];
+      }
+    }
+
+    const unsigned int width = LookupIntegerParameter(parameters, "WIDTH");
+    const unsigned int height = LookupIntegerParameter(parameters, "HEIGHT");
+    const unsigned int channelCount = LookupIntegerParameter(parameters, "DEPTH");
+    const unsigned int maxValue = LookupIntegerParameter(parameters, "MAXVAL");
+    const std::string tupleType = LookupStringParameter(parameters, "TUPLTYPE");
+
+    unsigned int bytesPerChannel;
+    PixelFormat format;
+    GetPixelFormat(format, bytesPerChannel, maxValue, channelCount, tupleType.c_str());
+
+    unsigned int pitch = width * channelCount * bytesPerChannel;
+
+    if (content_.size() != header.size() + headerDelimiter.size() + pitch * height)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    size_t offset = content_.size() - pitch * height;
+    AssignWritable(format, width, height, pitch, &content_[offset]);
+
+    // Byte swapping if needed
+    if (bytesPerChannel != 1 &&
+        bytesPerChannel != 2)
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+    
+    if (Toolbox::DetectEndianness() == Endianness_Little && bytesPerChannel == 2)
+    {
+      for (unsigned int h = 0; h < height; ++h)
+      {
+        uint16_t* pixel = reinterpret_cast<uint16_t*>(GetRow(h));
+        
+        for (unsigned int w = 0; w < GetWidth(); ++w, ++pixel)
+        {
+          *pixel = htobe16(*pixel);
+        }
+      }
+    }
+  }
+
+  
+#if ORTHANC_SANDBOXED == 0
+  void PamReader::ReadFromFile(const std::string& filename)
+  {
+    SystemToolbox::ReadFile(content_, filename);
+    ParseContent();
+  }
+#endif
+  
+
+  void PamReader::ReadFromMemory(const std::string& buffer)
+  {
+    content_ = buffer;
+    ParseContent();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Images/PamReader.h	Mon Jul 09 08:18:22 2018 +0200
@@ -0,0 +1,62 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., 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 "ImageAccessor.h"
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class PamReader :
+      public ImageAccessor,
+      public boost::noncopyable
+  {
+  private:
+    void ParseContent();
+    
+    std::string content_;
+
+  public:
+#if ORTHANC_SANDBOXED == 0
+    void ReadFromFile(const std::string& filename);
+#endif
+
+    void ReadFromMemory(const std::string& buffer);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Images/PamWriter.cpp	Mon Jul 09 08:18:22 2018 +0200
@@ -0,0 +1,153 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., 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/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "PamWriter.h"
+
+#include "../Endianness.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+#include <boost/lexical_cast.hpp>
+
+
+namespace Orthanc
+{
+  static void GetPixelFormatInfo(const PixelFormat& format,
+                                 unsigned int& maxValue,
+                                 unsigned int& channelCount,
+                                 unsigned int& bytesPerChannel,
+                                 std::string& tupleType)
+  {
+    switch (format)
+    {
+      case PixelFormat_Grayscale8:
+        maxValue = 255;
+        channelCount = 1;
+        bytesPerChannel = 1;
+        tupleType = "GRAYSCALE";
+        break;
+          
+      case PixelFormat_Grayscale16:
+        maxValue = 65535;
+        channelCount = 1;
+        bytesPerChannel = 2;
+        tupleType = "GRAYSCALE";
+        break;
+
+      case PixelFormat_RGB24:
+        maxValue = 255;
+        channelCount = 3;
+        bytesPerChannel = 1;
+        tupleType = "RGB";
+        break;
+
+      case PixelFormat_RGB48:
+        maxValue = 255;
+        channelCount = 3;
+        bytesPerChannel = 2;
+        tupleType = "RGB";
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+      
+  void PamWriter::WriteToMemoryInternal(std::string& target,
+                                        unsigned int width,
+                                        unsigned int height,
+                                        unsigned int sourcePitch,
+                                        PixelFormat format,
+                                        const void* buffer)
+  {
+    unsigned int maxValue, channelCount, bytesPerChannel;
+    std::string tupleType;
+    GetPixelFormatInfo(format, maxValue, channelCount, bytesPerChannel, tupleType);
+
+    target = (std::string("P7") +
+              std::string("\nWIDTH ")  + boost::lexical_cast<std::string>(width) + 
+              std::string("\nHEIGHT ") + boost::lexical_cast<std::string>(height) + 
+              std::string("\nDEPTH ")  + boost::lexical_cast<std::string>(channelCount) + 
+              std::string("\nMAXVAL ") + boost::lexical_cast<std::string>(maxValue) + 
+              std::string("\nTUPLTYPE ") + tupleType + 
+              std::string("\nENDHDR\n"));
+
+    if (bytesPerChannel != 1 &&
+        bytesPerChannel != 2)
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    size_t targetPitch = channelCount * bytesPerChannel * width;
+    size_t offset = target.size();
+
+    target.resize(offset + targetPitch * height);
+
+    assert(target.size() != 0);
+
+    if (Toolbox::DetectEndianness() == Endianness_Little &&
+        bytesPerChannel == 2)
+    {
+      // Byte swapping
+      for (unsigned int h = 0; h < height; ++h)
+      {
+        const uint16_t* p = reinterpret_cast<const uint16_t*>
+          (reinterpret_cast<const uint8_t*>(buffer) + h * sourcePitch);
+        uint16_t* q = reinterpret_cast<uint16_t*>
+          (reinterpret_cast<uint8_t*>(&target[offset]) + h * targetPitch);
+        
+        for (unsigned int w = 0; w < width * channelCount; ++w)
+        {
+          *q = htobe16(*p);
+          p++;
+          q++;
+        }
+      }
+    }
+    else
+    {
+      // Either "bytesPerChannel == 1" (and endianness is not
+      // relevant), or we run on a big endian architecture (and no
+      // byte swapping is necessary, as PAM uses big endian)
+      
+      for (unsigned int h = 0; h < height; ++h)
+      {
+        const void* p = reinterpret_cast<const uint8_t*>(buffer) + h * sourcePitch;
+        void* q = reinterpret_cast<uint8_t*>(&target[offset]) + h * targetPitch;
+        memcpy(q, p, targetPitch);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Images/PamWriter.h	Mon Jul 09 08:18:22 2018 +0200
@@ -0,0 +1,51 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., 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 "IImageWriter.h"
+
+namespace Orthanc
+{
+  // https://en.wikipedia.org/wiki/Netpbm#PAM_graphics_format
+  class PamWriter : public IImageWriter
+  {
+  protected:
+    virtual void WriteToMemoryInternal(std::string& target,
+                                       unsigned int width,
+                                       unsigned int height,
+                                       unsigned int pitch,
+                                       PixelFormat format,
+                                       const void* buffer);
+  };
+}
--- a/Core/Images/PngWriter.cpp	Thu Jul 05 17:10:10 2018 +0200
+++ b/Core/Images/PngWriter.cpp	Mon Jul 09 08:18:22 2018 +0200
@@ -248,7 +248,6 @@
 
 
 
-#if ORTHANC_SANDBOXED == 0
   void PngWriter::WriteToMemoryInternal(std::string& png,
                                         unsigned int width,
                                         unsigned int height,
@@ -272,5 +271,4 @@
 
     chunks.Flatten(png);
   }
-#endif
 }
--- a/Core/Images/PngWriter.h	Thu Jul 05 17:10:10 2018 +0200
+++ b/Core/Images/PngWriter.h	Mon Jul 09 08:18:22 2018 +0200
@@ -50,21 +50,21 @@
   class PngWriter : public IImageWriter
   {
   protected:
+#if ORTHANC_SANDBOXED == 0
     virtual void WriteToFileInternal(const std::string& filename,
                                      unsigned int width,
                                      unsigned int height,
                                      unsigned int pitch,
                                      PixelFormat format,
                                      const void* buffer);
+#endif
 
-#if ORTHANC_SANDBOXED == 0
     virtual void WriteToMemoryInternal(std::string& png,
                                        unsigned int width,
                                        unsigned int height,
                                        unsigned int pitch,
                                        PixelFormat format,
                                        const void* buffer);
-#endif
 
   private:
     struct PImpl;
--- a/NEWS	Thu Jul 05 17:10:10 2018 +0200
+++ b/NEWS	Mon Jul 09 08:18:22 2018 +0200
@@ -32,6 +32,9 @@
   for data already in Orthanc, you'll need to reconstruct the data by
   sending a POST request to the ".../reconstruct" URI.  This change
   triggered an update of ORTHANC_API_VERSION from 1.0 to 1.1
+* "/instances/.../frame/../image-uint8 and friends now accepts a 
+  "image/pam" MIME type to retrieve images in PAM format
+  (https://en.wikipedia.org/wiki/Netpbm#PAM_graphics_format)
 
 Plugins
 -------
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Thu Jul 05 17:10:10 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Mon Jul 09 08:18:22 2018 +0200
@@ -364,6 +364,20 @@
         DicomImageDecoder::ExtractPngImage(answer_, image_, mode_, invert_);
       }
 
+      void EncodeUsingPam()
+      {
+        /**
+         * "No Internet Media Type (aka MIME type, content type) for
+         * PBM has been registered with IANA, but the unofficial value
+         * image/x-portable-arbitrarymap is assigned by this
+         * specification, to be consistent with conventional values
+         * for the older Netpbm formats."
+         * http://netpbm.sourceforge.net/doc/pam.html
+         **/
+        format_ = "image/x-portable-arbitrarymap";
+        DicomImageDecoder::ExtractPamImage(answer_, image_, mode_, invert_);
+      }
+
       void EncodeUsingJpeg(uint8_t quality)
       {
         format_ = "image/jpeg";
@@ -390,6 +404,25 @@
       }
     };
 
+    class EncodePam : public HttpContentNegociation::IHandler
+    {
+    private:
+      ImageToEncode&  image_;
+
+    public:
+      EncodePam(ImageToEncode& image) : image_(image)
+      {
+      }
+
+      virtual void Handle(const std::string& type,
+                          const std::string& subtype)
+      {
+        assert(type == "image");
+        assert(subtype == "pam");
+        image_.EncodeUsingPam();
+      }
+    };
+
     class EncodeJpeg : public HttpContentNegociation::IHandler
     {
     private:
@@ -524,8 +557,14 @@
     ImageToEncode image(decoded, mode, invert);
 
     HttpContentNegociation negociation;
-    EncodePng png(image);          negociation.Register("image/png", png);
-    EncodeJpeg jpeg(image, call);  negociation.Register("image/jpeg", jpeg);
+    EncodePng png(image);
+    negociation.Register("image/png", png);
+
+    EncodeJpeg jpeg(image, call);
+    negociation.Register("image/jpeg", jpeg);
+
+    EncodePam pam(image);
+    negociation.Register("image/x-portable-arbitrarymap", pam);
 
     if (negociation.Apply(call.GetHttpHeaders()))
     {
--- a/OrthancServer/main.cpp	Thu Jul 05 17:10:10 2018 +0200
+++ b/OrthancServer/main.cpp	Mon Jul 09 08:18:22 2018 +0200
@@ -1049,7 +1049,7 @@
   database.Open();
 
   unsigned int currentVersion = database.GetDatabaseVersion();
-  
+
   if (upgradeDatabase)
   {
     UpgradeDatabase(database, storageArea);
--- a/Resources/CMake/OrthancFrameworkConfiguration.cmake	Thu Jul 05 17:10:10 2018 +0200
+++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake	Mon Jul 09 08:18:22 2018 +0200
@@ -140,6 +140,8 @@
   ${ORTHANC_ROOT}/Core/Images/ImageAccessor.cpp
   ${ORTHANC_ROOT}/Core/Images/ImageBuffer.cpp
   ${ORTHANC_ROOT}/Core/Images/ImageProcessing.cpp
+  ${ORTHANC_ROOT}/Core/Images/PamReader.cpp
+  ${ORTHANC_ROOT}/Core/Images/PamWriter.cpp
   ${ORTHANC_ROOT}/Core/JobsEngine/GenericJobUnserializer.cpp
   ${ORTHANC_ROOT}/Core/JobsEngine/JobInfo.cpp
   ${ORTHANC_ROOT}/Core/JobsEngine/JobStatus.cpp
--- a/UnitTestsSources/ImageTests.cpp	Thu Jul 05 17:10:10 2018 +0200
+++ b/UnitTestsSources/ImageTests.cpp	Mon Jul 09 08:18:22 2018 +0200
@@ -41,6 +41,8 @@
 #include "../Core/Images/JpegWriter.h"
 #include "../Core/Images/PngReader.h"
 #include "../Core/Images/PngWriter.h"
+#include "../Core/Images/PamReader.h"
+#include "../Core/Images/PamWriter.h"
 #include "../Core/Toolbox.h"
 #include "../Core/TemporaryFile.h"
 #include "../OrthancServer/OrthancInitialization.h"  // For the FontRegistry
@@ -269,3 +271,161 @@
   w.WriteToFile("UnitTestsResults/font.png", s);
 }
 
+TEST(PamWriter, ColorPattern)
+{
+  Orthanc::PamWriter w;
+  unsigned int width = 17;
+  unsigned int height = 61;
+  unsigned int pitch = width * 3;
+
+  std::vector<uint8_t> image(height * pitch);
+  for (unsigned int y = 0; y < height; y++)
+  {
+    uint8_t *p = &image[0] + y * pitch;
+    for (unsigned int x = 0; x < width; x++, p += 3)
+    {
+      p[0] = (y % 3 == 0) ? 255 : 0;
+      p[1] = (y % 3 == 1) ? 255 : 0;
+      p[2] = (y % 3 == 2) ? 255 : 0;
+    }
+  }
+
+  Orthanc::ImageAccessor accessor;
+  accessor.AssignReadOnly(Orthanc::PixelFormat_RGB24, width, height, pitch, &image[0]);
+
+  w.WriteToFile("UnitTestsResults/ColorPattern.pam", accessor);
+
+  std::string f, md5;
+  Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/ColorPattern.pam");
+  Orthanc::Toolbox::ComputeMD5(md5, f);
+  ASSERT_EQ("81a3441754e88969ebbe53e69891e841", md5);
+}
+
+TEST(PamWriter, Gray8Pattern)
+{
+  Orthanc::PamWriter w;
+  int width = 17;
+  int height = 256;
+  int pitch = width;
+
+  std::vector<uint8_t> image(height * pitch);
+  for (int y = 0; y < height; y++)
+  {
+    uint8_t *p = &image[0] + y * pitch;
+    for (int x = 0; x < width; x++, p++)
+    {
+      *p = y;
+    }
+  }
+
+  Orthanc::ImageAccessor accessor;
+  accessor.AssignReadOnly(Orthanc::PixelFormat_Grayscale8, width, height, pitch, &image[0]);
+
+  w.WriteToFile("UnitTestsResults/Gray8Pattern.pam", accessor);
+
+  std::string f, md5;
+  Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray8Pattern.pam");
+  Orthanc::Toolbox::ComputeMD5(md5, f);
+  ASSERT_EQ("7873c408d26a9d11dd1c1de5e69cc0a3", md5);
+}
+
+TEST(PamWriter, Gray16Pattern)
+{
+  Orthanc::PamWriter w;
+  int width = 256;
+  int height = 256;
+  int pitch = width * 2 + 16;
+
+  std::vector<uint8_t> image(height * pitch);
+
+  int v = 0;
+  for (int y = 0; y < height; y++)
+  {
+    uint16_t *p = reinterpret_cast<uint16_t*>(&image[0] + y * pitch);
+    for (int x = 0; x < width; x++, p++, v++)
+    {
+      *p = v;
+    }
+  }
+
+  Orthanc::ImageAccessor accessor;
+  accessor.AssignReadOnly(Orthanc::PixelFormat_Grayscale16, width, height, pitch, &image[0]);
+  w.WriteToFile("UnitTestsResults/Gray16Pattern.pam", accessor);
+
+  std::string f, md5;
+  Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray16Pattern.pam");
+  Orthanc::Toolbox::ComputeMD5(md5, f);
+  ASSERT_EQ("b268772bf28f3b2b8520ff21c5e3dcb6", md5);
+}
+
+TEST(PamWriter, EndToEnd)
+{
+  Orthanc::PamWriter w;
+  unsigned int width = 256;
+  unsigned int height = 256;
+  unsigned int pitch = width * 2 + 16;
+
+  std::vector<uint8_t> image(height * pitch);
+
+  int v = 0;
+  for (unsigned int y = 0; y < height; y++)
+  {
+    uint16_t *p = reinterpret_cast<uint16_t*>(&image[0] + y * pitch);
+    for (unsigned int x = 0; x < width; x++, p++, v++)
+    {
+      *p = v;
+    }
+  }
+
+  Orthanc::ImageAccessor accessor;
+  accessor.AssignReadOnly(Orthanc::PixelFormat_Grayscale16, width, height, pitch, &image[0]);
+
+  std::string s;
+  w.WriteToMemory(s, accessor);
+
+  {
+    Orthanc::PamReader r;
+    r.ReadFromMemory(s);
+
+    ASSERT_EQ(r.GetFormat(), Orthanc::PixelFormat_Grayscale16);
+    ASSERT_EQ(r.GetWidth(), width);
+    ASSERT_EQ(r.GetHeight(), height);
+
+    v = 0;
+    for (unsigned int y = 0; y < height; y++)
+    {
+      const uint16_t *p = reinterpret_cast<const uint16_t*>
+        ((const uint8_t*) r.GetConstBuffer() + y * r.GetPitch());
+      ASSERT_EQ(p, r.GetConstRow(y));
+      for (unsigned int x = 0; x < width; x++, p++, v++)
+      {
+        ASSERT_EQ(v, *p);
+      }
+    }
+  }
+
+  {
+    Orthanc::TemporaryFile tmp;
+    Orthanc::SystemToolbox::WriteFile(s, tmp.GetPath());
+
+    Orthanc::PamReader r2;
+    r2.ReadFromFile(tmp.GetPath());
+
+    ASSERT_EQ(r2.GetFormat(), Orthanc::PixelFormat_Grayscale16);
+    ASSERT_EQ(r2.GetWidth(), width);
+    ASSERT_EQ(r2.GetHeight(), height);
+
+    v = 0;
+    for (unsigned int y = 0; y < height; y++)
+    {
+      const uint16_t *p = reinterpret_cast<const uint16_t*>
+        ((const uint8_t*) r2.GetConstBuffer() + y * r2.GetPitch());
+      ASSERT_EQ(p, r2.GetConstRow(y));
+      for (unsigned int x = 0; x < width; x++, p++, v++)
+      {
+        ASSERT_EQ(*p, v);
+      }
+    }
+  }
+}
+