changeset 2699:52217dc47a4e

new image/pam MIME TYPE supported in /instances/../frames/../image-uint8... routes
author am@osimis.io
date Thu, 05 Jul 2018 12:25:00 +0200
parents 2c684841da15
children be6d7acf4e94
files .hgignore Core/DicomParsing/Internals/DicomImageDecoder.cpp Core/DicomParsing/Internals/DicomImageDecoder.h Core/Images/JpegWriter.cpp Core/Images/JpegWriter.h Core/Images/PamReader.cpp Core/Images/PamReader.h Core/Images/PamWriter.cpp Core/Images/PamWriter.h Core/Images/PngWriter.cpp Core/Images/PngWriter.h NEWS OrthancServer/OrthancRestApi/OrthancRestResources.cpp OrthancServer/main.cpp Resources/CMake/OrthancFrameworkConfiguration.cmake UnitTestsSources/ImageTests.cpp
diffstat 16 files changed, 715 insertions(+), 10 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Thu Jun 28 12:24:45 2018 +0200
+++ b/.hgignore	Thu Jul 05 12:25:00 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 Jun 28 12:24:45 2018 +0200
+++ b/Core/DicomParsing/Internals/DicomImageDecoder.cpp	Thu Jul 05 12:25:00 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 Jun 28 12:24:45 2018 +0200
+++ b/Core/DicomParsing/Internals/DicomImageDecoder.h	Thu Jul 05 12:25:00 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/Images/JpegWriter.cpp	Thu Jun 28 12:24:45 2018 +0200
+++ b/Core/Images/JpegWriter.cpp	Thu Jul 05 12:25:00 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 Jun 28 12:24:45 2018 +0200
+++ b/Core/Images/JpegWriter.h	Thu Jul 05 12:25:00 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	Thu Jul 05 12:25:00 2018 +0200
@@ -0,0 +1,184 @@
+/**
+ * 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 "../OrthancException.h"
+#include "../Toolbox.h"
+#include <istream>
+#include <sstream>
+#include <fstream>
+#include <endian.h>
+#if ORTHANC_SANDBOXED == 0
+#  include "../SystemToolbox.h"
+#endif
+
+#include <string.h>
+
+namespace Orthanc
+{
+  namespace
+  {
+    void GetPixelFormat(PixelFormat& format, unsigned int& bytesPerChannel, const unsigned int& maxValue, const unsigned int& channelCount, const char* tupleType)
+    {
+      if (strcmp(tupleType, "GRAYSCALE") == 0 && channelCount == 1)
+      {
+        if (maxValue == 255)
+        {
+          format = PixelFormat_Grayscale8;
+          bytesPerChannel = 1;
+          return;
+        }
+        else if (maxValue == 65535)
+        {
+          format = PixelFormat_Grayscale16;
+          bytesPerChannel = 2;
+          return;
+        }
+      }
+      else if (strcmp(tupleType, "RGB") == 0 && channelCount == 3)
+      {
+        if (maxValue == 255)
+        {
+          format = PixelFormat_RGB24;
+          bytesPerChannel = 1;
+          return;
+        }
+        else if (maxValue == 65535)
+        {
+          format = PixelFormat_RGB48;
+          bytesPerChannel = 2;
+          return;
+        }
+      }
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    void ReadDelimiter(std::istream& input, const char* expectedDelimiter)
+    {
+      std::string delimiter;
+      input >> delimiter;
+      if (delimiter != expectedDelimiter)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+
+    unsigned int ReadKeyValueUint(std::istream& input, const char* expectedKey)
+    {
+      std::string key;
+      unsigned int value;
+      input >> key >> value;
+      if (key != expectedKey)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      return value;
+    }
+
+    std::string ReadKeyValueString(std::istream& input, const char* expectedKey)
+    {
+      std::string key;
+      std::string value;
+      input >> key >> value;
+      if (key != expectedKey)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      return value;
+    }
+  }
+
+  void PamReader::ReadFromStream(std::istream& input)
+  {
+    ReadDelimiter(input, "P7");
+    unsigned int width = ReadKeyValueUint(input, "WIDTH");
+    unsigned int height = ReadKeyValueUint(input, "HEIGHT");
+    unsigned int channelCount = ReadKeyValueUint(input, "DEPTH");
+    unsigned int maxValue = ReadKeyValueUint(input, "MAXVAL");
+    std::string tupleType = ReadKeyValueString(input, "TUPLTYPE");
+    ReadDelimiter(input, "ENDHDR");
+    // skip last EOL
+    char tmp[16];
+    input.getline(tmp, 16);
+
+    unsigned int bytesPerChannel;
+    PixelFormat format;
+    GetPixelFormat(format, bytesPerChannel, maxValue, channelCount, tupleType.c_str());
+
+    // read the pixels data
+    unsigned int sizeInBytes = width * height * channelCount * bytesPerChannel;
+    data_.reserve(sizeInBytes);
+    input.read(data_.data(), sizeInBytes);
+
+    AssignWritable(format, width, height, width * channelCount * bytesPerChannel, data_.data());
+
+    // swap bytes
+    if (Toolbox::DetectEndianness() == Endianness_Little && bytesPerChannel == 2)
+    {
+      uint16_t* pixel = NULL;
+      for (unsigned int h = 0; h < height; ++h)
+      {
+        pixel = reinterpret_cast<uint16_t*> (data_.data() + h * width * channelCount * bytesPerChannel);
+        for (unsigned int w = 0; w < (width * channelCount); ++w, ++pixel)
+        {
+          *pixel = htobe16(*pixel);
+        }
+      }
+    }
+
+  }
+
+#if ORTHANC_SANDBOXED == 0
+  void PamReader::ReadFromFile(const std::string& filename)
+  {
+    std::ifstream inputStream(filename, std::ofstream::binary);
+    ReadFromStream(inputStream);
+  }
+#endif
+
+  void PamReader::ReadFromMemory(const void* buffer,
+                                 size_t size)
+  {
+    std::istringstream inputStream(std::string(reinterpret_cast<const char*>(buffer), size));
+    ReadFromStream(inputStream);
+  }
+
+  void PamReader::ReadFromMemory(const std::string& buffer)
+  {
+    std::istringstream inputStream(buffer);
+    ReadFromStream(inputStream);
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Images/PamReader.h	Thu Jul 05 12:25:00 2018 +0200
@@ -0,0 +1,83 @@
+/**
+ * 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
+
+#if !defined(ORTHANC_ENABLE_PNG)
+#  error The macro ORTHANC_ENABLE_PNG must be defined
+#endif
+
+#if ORTHANC_ENABLE_PNG != 1
+#  error PNG support must be enabled to include this file
+#endif
+
+#include "ImageAccessor.h"
+
+#include "../Enumerations.h"
+
+#include <vector>
+#include <stdint.h>
+#include <boost/shared_ptr.hpp>
+#include <boost/noncopyable.hpp>
+#include <istream>
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+namespace Orthanc
+{
+  class PamReader :
+      public ImageAccessor,
+      public boost::noncopyable
+  {
+  private:
+    std::vector<char> data_;
+
+  public:
+    PamReader() {}
+    virtual ~PamReader() {}
+
+#if ORTHANC_SANDBOXED == 0
+    void ReadFromFile(const std::string& filename);
+#endif
+
+    void ReadFromMemory(const void* buffer,
+                        size_t size);
+
+    void ReadFromMemory(const std::string& buffer);
+
+  protected:
+    void ReadFromStream(std::istream& input);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Images/PamWriter.cpp	Thu Jul 05 12:25:00 2018 +0200
@@ -0,0 +1,168 @@
+/**
+ * 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 <vector>
+#include <stdint.h>
+#include <iostream>
+#include <sstream>
+#include <fstream>
+#include <endian.h>
+#include "../OrthancException.h"
+#include "../ChunkedBuffer.h"
+#include "../Toolbox.h"
+
+#if ORTHANC_SANDBOXED == 0
+#  include "../SystemToolbox.h"
+#endif
+
+
+namespace Orthanc
+{
+  namespace
+  {
+    void GetPixelFormatInfo(const PixelFormat& format, unsigned int& maxValue, unsigned int& channelCount, unsigned int& bytesPerChannel, const char*& tupleType) {
+      maxValue = 255;
+      channelCount = 1;
+      bytesPerChannel = 1;
+      tupleType = NULL;
+
+      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 WriteToStream(std::ostream& output,
+                       unsigned int width,
+                       unsigned int height,
+                       unsigned int pitch,
+                       PixelFormat format,
+                       const void* buffer)
+    {
+      unsigned int maxValue = 255;
+      unsigned int channelCount = 1;
+      unsigned int bytesPerChannel = 1;
+      const char* tupleType = "GRAYSCALE";
+      GetPixelFormatInfo(format, maxValue, channelCount, bytesPerChannel, tupleType);
+
+      output << "P7" << "\n";
+      output << "WIDTH " << width << "\n";
+      output << "HEIGHT " << height << "\n";
+      output << "DEPTH " << channelCount << "\n";
+      output << "MAXVAL " << maxValue << "\n";
+      output << "TUPLTYPE " << tupleType << "\n";
+      output << "ENDHDR" << "\n";
+
+      if (Toolbox::DetectEndianness() == Endianness_Little && bytesPerChannel == 2)
+      {
+        uint16_t tmp;
+        const uint16_t* pixel = NULL;
+        for (unsigned int h = 0; h < height; ++h)
+        {
+          pixel = reinterpret_cast<const uint16_t*> (reinterpret_cast<const uint8_t*>(buffer) + h * pitch);
+          for (unsigned int w = 0; w < (width * channelCount); ++w, ++pixel)
+          {
+            tmp = htobe16(*pixel);
+            output.write(reinterpret_cast<const char*>(&tmp), 2);
+          }
+        }
+      }
+      else
+      {
+        for (unsigned int h = 0; h < height; ++h)
+        {
+          output.write(reinterpret_cast<const char*>(reinterpret_cast<const uint8_t*>(buffer) + h * pitch), channelCount * bytesPerChannel * width);
+        }
+      }
+
+    }
+
+  }
+
+#if ORTHANC_SANDBOXED == 0
+  void PamWriter::WriteToFileInternal(const std::string& filename,
+                                      unsigned int width,
+                                      unsigned int height,
+                                      unsigned int pitch,
+                                      PixelFormat format,
+                                      const void* buffer)
+  {
+    std::ofstream outfile (filename, std::ofstream::binary);
+
+    WriteToStream(outfile, width, height, pitch, format, buffer);
+    outfile.close();
+  }
+#endif
+
+
+  void PamWriter::WriteToMemoryInternal(std::string& output,
+                                        unsigned int width,
+                                        unsigned int height,
+                                        unsigned int pitch,
+                                        PixelFormat format,
+                                        const void* buffer)
+  {
+    std::ostringstream outStream;  // todo: try to write directly in output and avoid copy
+
+    WriteToStream(outStream, width, height, pitch, format, buffer);
+    output = outStream.str();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Images/PamWriter.h	Thu Jul 05 12:25:00 2018 +0200
@@ -0,0 +1,65 @@
+/**
+ * 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:
+#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
+
+    virtual void WriteToMemoryInternal(std::string& png,
+                                       unsigned int width,
+                                       unsigned int height,
+                                       unsigned int pitch,
+                                       PixelFormat format,
+                                       const void* buffer);
+
+  public:
+    PamWriter() {}
+
+    virtual ~PamWriter() {}
+  };
+}
--- a/Core/Images/PngWriter.cpp	Thu Jun 28 12:24:45 2018 +0200
+++ b/Core/Images/PngWriter.cpp	Thu Jul 05 12:25:00 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 Jun 28 12:24:45 2018 +0200
+++ b/Core/Images/PngWriter.h	Thu Jul 05 12:25:00 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 Jun 28 12:24:45 2018 +0200
+++ b/NEWS	Thu Jul 05 12:25:00 2018 +0200
@@ -22,6 +22,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)
 
 Maintenance
 -----------
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Thu Jun 28 12:24:45 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Thu Jul 05 12:25:00 2018 +0200
@@ -364,6 +364,12 @@
         DicomImageDecoder::ExtractPngImage(answer_, image_, mode_, invert_);
       }
 
+      void EncodeUsingPam()
+      {
+        format_ = "image/pam";
+        DicomImageDecoder::ExtractPamImage(answer_, image_, mode_, invert_);
+      }
+
       void EncodeUsingJpeg(uint8_t quality)
       {
         format_ = "image/jpeg";
@@ -390,6 +396,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:
@@ -526,6 +551,7 @@
     HttpContentNegociation negociation;
     EncodePng png(image);          negociation.Register("image/png", png);
     EncodeJpeg jpeg(image, call);  negociation.Register("image/jpeg", jpeg);
+    EncodePam pam(image);          negociation.Register("image/pam", pam);
 
     if (negociation.Apply(call.GetHttpHeaders()))
     {
--- a/OrthancServer/main.cpp	Thu Jun 28 12:24:45 2018 +0200
+++ b/OrthancServer/main.cpp	Thu Jul 05 12:25:00 2018 +0200
@@ -1057,7 +1057,7 @@
   database.Open();
 
   unsigned int currentVersion = database.GetDatabaseVersion();
-  
+
   if (upgradeDatabase)
   {
     UpgradeDatabase(database, storageArea);
--- a/Resources/CMake/OrthancFrameworkConfiguration.cmake	Thu Jun 28 12:24:45 2018 +0200
+++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake	Thu Jul 05 12:25:00 2018 +0200
@@ -137,6 +137,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/Logging.cpp
   ${ORTHANC_ROOT}/Core/Toolbox.cpp
   ${ORTHANC_ROOT}/Core/WebServiceParameters.cpp
--- a/UnitTestsSources/ImageTests.cpp	Thu Jun 28 12:24:45 2018 +0200
+++ b/UnitTestsSources/ImageTests.cpp	Thu Jul 05 12:25:00 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,159 @@
   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);
+      }
+    }
+  }
+}
+