# HG changeset patch # User Sebastien Jodogne # Date 1447778792 -3600 # Node ID 5ad4e4d92ecb64562cc9fd3c18c6c3047720180f # Parent 94990da8710e3017d9249ee4763afb08b2b9ee77 AcceptMediaDispatcher bootstrap diff -r 94990da8710e -r 5ad4e4d92ecb Core/Enumerations.cpp --- a/Core/Enumerations.cpp Fri Nov 13 15:06:45 2015 +0100 +++ b/Core/Enumerations.cpp Tue Nov 17 17:46:32 2015 +0100 @@ -151,6 +151,9 @@ case ErrorCode_EmptyRequest: return "The request is empty"; + case ErrorCode_NotAcceptable: + return "Cannot send a response which is acceptable according to the Accept HTTP header"; + case ErrorCode_SQLiteNotOpened: return "SQLite: The database is not opened"; @@ -1131,6 +1134,9 @@ case ErrorCode_Unauthorized: return HttpStatus_401_Unauthorized; + case ErrorCode_NotAcceptable: + return HttpStatus_406_NotAcceptable; + default: return HttpStatus_500_InternalServerError; } diff -r 94990da8710e -r 5ad4e4d92ecb Core/Enumerations.h --- a/Core/Enumerations.h Fri Nov 13 15:06:45 2015 +0100 +++ b/Core/Enumerations.h Tue Nov 17 17:46:32 2015 +0100 @@ -80,6 +80,7 @@ ErrorCode_DatabasePlugin = 31 /*!< The plugin implementing a custom database back-end does not fulfill the proper interface */, ErrorCode_StorageAreaPlugin = 32 /*!< Error in the plugin implementing a custom storage area */, ErrorCode_EmptyRequest = 33 /*!< The request is empty */, + ErrorCode_NotAcceptable = 34 /*!< Cannot send a response which is acceptable according to the Accept HTTP header */, ErrorCode_SQLiteNotOpened = 1000 /*!< SQLite: The database is not opened */, ErrorCode_SQLiteAlreadyOpened = 1001 /*!< SQLite: Connection is already open */, ErrorCode_SQLiteCannotOpen = 1002 /*!< SQLite: Unable to open the database */, diff -r 94990da8710e -r 5ad4e4d92ecb OrthancServer/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Fri Nov 13 15:06:45 2015 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Tue Nov 17 17:46:32 2015 +0100 @@ -259,6 +259,32 @@ } + static void AnswerImage(RestApiGetCall& call, + ParsedDicomFile& dicom, + unsigned int frame, + ImageExtractionMode mode) + { + typedef std::vector MediaRanges; + + // Get the HTTP "Accept" header, if any + std::string accept = call.GetHttpHeader("accept", "*/*"); + + MediaRanges mediaRanges; + Toolbox::TokenizeString(mediaRanges, accept, ','); + + for (MediaRanges::const_reverse_iterator it = mediaRanges.rbegin(); + it != mediaRanges.rend(); ++it) + { + } + + throw OrthancException(ErrorCode_NotAcceptable); + + std::string image; + dicom.ExtractPngImage(image, frame, mode); + call.GetOutput().AnswerBuffer(image, "image/png"); + } + + template static void GetImage(RestApiGetCall& call) { @@ -277,15 +303,14 @@ } std::string publicId = call.GetUriComponent("id", ""); - std::string dicomContent, png; + std::string dicomContent; context.ReadFile(dicomContent, publicId, FileContentType_Dicom); ParsedDicomFile dicom(dicomContent); try { - dicom.ExtractPngImage(png, frame, mode); - call.GetOutput().AnswerBuffer(png, "image/png"); + AnswerImage(call, dicom, frame, mode); } catch (OrthancException& e) { diff -r 94990da8710e -r 5ad4e4d92ecb OrthancServer/ParsedDicomFile.cpp --- a/OrthancServer/ParsedDicomFile.cpp Fri Nov 13 15:06:45 2015 +0100 +++ b/OrthancServer/ParsedDicomFile.cpp Tue Nov 17 17:46:32 2015 +0100 @@ -84,14 +84,16 @@ #include "FromDcmtkBridge.h" #include "ToDcmtkBridge.h" #include "Internals/DicomImageDecoder.h" -#include "../Core/Logging.h" -#include "../Core/Toolbox.h" -#include "../Core/OrthancException.h" +#include "../Core/DicomFormat/DicomIntegerPixelAccessor.h" #include "../Core/Images/ImageBuffer.h" +#include "../Core/Images/JpegWriter.h" +#include "../Core/Images/JpegReader.h" +#include "../Core/Images/PngReader.h" #include "../Core/Images/PngWriter.h" +#include "../Core/Logging.h" +#include "../Core/OrthancException.h" +#include "../Core/Toolbox.h" #include "../Core/Uuid.h" -#include "../Core/DicomFormat/DicomIntegerPixelAccessor.h" -#include "../Core/Images/PngReader.h" #include #include @@ -796,36 +798,6 @@ } - template - static void ExtractPngImageTruncate(std::string& result, - DicomIntegerPixelAccessor& accessor, - PixelFormat format) - { - assert(accessor.GetInformation().GetChannelCount() == 1); - - PngWriter w; - - std::vector image(accessor.GetInformation().GetWidth() * accessor.GetInformation().GetHeight(), 0); - T* pixel = &image[0]; - for (unsigned int y = 0; y < accessor.GetInformation().GetHeight(); y++) - { - for (unsigned int x = 0; x < accessor.GetInformation().GetWidth(); x++, pixel++) - { - int32_t v = accessor.GetValue(x, y); - if (v < static_cast(std::numeric_limits::min())) - *pixel = std::numeric_limits::min(); - else if (v > static_cast(std::numeric_limits::max())) - *pixel = std::numeric_limits::max(); - else - *pixel = static_cast(v); - } - } - - w.WriteToMemory(result, accessor.GetInformation().GetWidth(), accessor.GetInformation().GetHeight(), - accessor.GetInformation().GetWidth() * sizeof(T), format, &image[0]); - } - - void ParsedDicomFile::SaveToMemoryBuffer(std::string& buffer) { FromDcmtkBridge::SaveToMemoryBuffer(buffer, *pimpl_->file_->getDataset()); @@ -903,7 +875,8 @@ Toolbox::DecodeDataUriScheme(mime, content, dataUriScheme); Toolbox::ToLowerCase(mime); - if (mime == "image/png") + if (mime == "image/png" || + mime == "image/jpeg") { EmbedImage(mime, content); } @@ -928,6 +901,12 @@ reader.ReadFromMemory(content); EmbedImage(reader); } + else if (mime == "image/jpeg") + { + JpegReader reader; + reader.ReadFromMemory(content); + EmbedImage(reader); + } else { throw OrthancException(ErrorCode_NotImplemented); @@ -1100,6 +1079,27 @@ } + void ParsedDicomFile::ExtractJpegImage(std::string& result, + unsigned int frame, + ImageExtractionMode mode, + uint8_t quality) + { + if (mode != ImageExtractionMode_UInt8 && + mode != ImageExtractionMode_Preview) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + ImageBuffer buffer; + ExtractImage(buffer, frame, mode); + + ImageAccessor accessor(buffer.GetConstAccessor()); + JpegWriter writer; + writer.SetQuality(quality); + writer.WriteToMemory(result, accessor); + } + + Encoding ParsedDicomFile::GetEncoding() const { return FromDcmtkBridge::DetectEncoding(*pimpl_->file_->getDataset()); diff -r 94990da8710e -r 5ad4e4d92ecb OrthancServer/ParsedDicomFile.h --- a/OrthancServer/ParsedDicomFile.h Fri Nov 13 15:06:45 2015 +0100 +++ b/OrthancServer/ParsedDicomFile.h Tue Nov 17 17:46:32 2015 +0100 @@ -129,6 +129,11 @@ unsigned int frame, ImageExtractionMode mode); + void ExtractJpegImage(std::string& result, + unsigned int frame, + ImageExtractionMode mode, + uint8_t quality); + Encoding GetEncoding() const; void SetEncoding(Encoding encoding); diff -r 94990da8710e -r 5ad4e4d92ecb OrthancServer/main.cpp --- a/OrthancServer/main.cpp Fri Nov 13 15:06:45 2015 +0100 +++ b/OrthancServer/main.cpp Tue Nov 17 17:46:32 2015 +0100 @@ -492,6 +492,7 @@ PrintErrorCode(ErrorCode_DatabasePlugin, "The plugin implementing a custom database back-end does not fulfill the proper interface"); PrintErrorCode(ErrorCode_StorageAreaPlugin, "Error in the plugin implementing a custom storage area"); PrintErrorCode(ErrorCode_EmptyRequest, "The request is empty"); + PrintErrorCode(ErrorCode_NotAcceptable, "Cannot send a response which is acceptable according to the Accept HTTP header"); PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened"); PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open"); PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database"); diff -r 94990da8710e -r 5ad4e4d92ecb Plugins/Include/orthanc/OrthancCPlugin.h --- a/Plugins/Include/orthanc/OrthancCPlugin.h Fri Nov 13 15:06:45 2015 +0100 +++ b/Plugins/Include/orthanc/OrthancCPlugin.h Tue Nov 17 17:46:32 2015 +0100 @@ -213,6 +213,7 @@ OrthancPluginErrorCode_DatabasePlugin = 31 /*!< The plugin implementing a custom database back-end does not fulfill the proper interface */, OrthancPluginErrorCode_StorageAreaPlugin = 32 /*!< Error in the plugin implementing a custom storage area */, OrthancPluginErrorCode_EmptyRequest = 33 /*!< The request is empty */, + OrthancPluginErrorCode_NotAcceptable = 34 /*!< Cannot send a response which is acceptable according to the Accept HTTP header */, OrthancPluginErrorCode_SQLiteNotOpened = 1000 /*!< SQLite: The database is not opened */, OrthancPluginErrorCode_SQLiteAlreadyOpened = 1001 /*!< SQLite: Connection is already open */, OrthancPluginErrorCode_SQLiteCannotOpen = 1002 /*!< SQLite: Unable to open the database */, diff -r 94990da8710e -r 5ad4e4d92ecb Resources/ErrorCodes.json --- a/Resources/ErrorCodes.json Fri Nov 13 15:06:45 2015 +0100 +++ b/Resources/ErrorCodes.json Tue Nov 17 17:46:32 2015 +0100 @@ -189,6 +189,12 @@ "Code": 33, "Name": "EmptyRequest", "Description": "The request is empty" + }, + { + "Code": 34, + "HttpStatus": 406, + "Name": "NotAcceptable", + "Description": "Cannot send a response which is acceptable according to the Accept HTTP header" }, diff -r 94990da8710e -r 5ad4e4d92ecb UnitTestsSources/RestApiTests.cpp --- a/UnitTestsSources/RestApiTests.cpp Fri Nov 13 15:06:45 2015 +0100 +++ b/UnitTestsSources/RestApiTests.cpp Tue Nov 17 17:46:32 2015 +0100 @@ -34,6 +34,8 @@ #include "gtest/gtest.h" #include +#include +#include #include "../Core/ChunkedBuffer.h" #include "../Core/HttpClient.h" @@ -335,3 +337,402 @@ ASSERT_TRUE(HandleGet(root, "/hello2/a/b")); ASSERT_EQ(testValue, 4); } + + + + +namespace Orthanc +{ + class AcceptMediaDispatcher : public boost::noncopyable + { + public: + typedef std::map HttpHeaders; + + class IHandler : public boost::noncopyable + { + public: + virtual ~IHandler() + { + } + + virtual void Handle(const std::string& type, + const std::string& subtype, + float quality /* between 0 and 1 */) = 0; + }; + + private: + struct Handler + { + std::string type_; + std::string subtype_; + IHandler& handler_; + + Handler(const std::string& type, + const std::string& subtype, + IHandler& handler) : + type_(type), + subtype_(subtype), + handler_(handler) + { + } + + bool IsMatch(const std::string& type, + const std::string& subtype) const + { + if (type == "*" && subtype == "*") + { + return true; + } + + if (subtype == "*" && type == type_) + { + return true; + } + + return type == type_ && subtype == subtype_; + } + }; + + + struct Reference : public boost::noncopyable + { + const Handler& handler_; + uint8_t level_; + float quality_; + size_t specificity_; // Number of arguments + + Reference(const Handler& handler, + const std::string& type, + const std::string& subtype, + float quality, + size_t specificity) : + handler_(handler), + quality_(quality), + specificity_(specificity) + { + if (type == "*" && subtype == "*") + { + level_ = 0; + } + else if (subtype == "*") + { + level_ = 1; + } + else + { + level_ = 2; + } + } + + bool operator< (const Reference& other) const + { + if (level_ < other.level_) + { + return true; + } + + if (level_ > other.level_) + { + return false; + } + + return specificity_ < other.specificity_; + } + + void Call() const + { + handler_.handler_.Handle(handler_.type_, handler_.subtype_, quality_); + } + }; + + + typedef std::vector Tokens; + typedef std::list Handlers; + + Handlers handlers_; + + + static bool SplitPair(std::string& first /* out */, + std::string& second /* out */, + const std::string& source, + char separator) + { + size_t pos = source.find(separator); + + if (pos == std::string::npos) + { + return false; + } + else + { + first = Toolbox::StripSpaces(source.substr(0, pos)); + second = Toolbox::StripSpaces(source.substr(pos + 1)); + return true; + } + } + + + static void GetQualityAndSpecificity(float& quality /* out */, + size_t& specificity /* out */, + const Tokens& parameters) + { + assert(!parameters.empty()); + + quality = 1.0f; + specificity = parameters.size() - 1; + + for (size_t i = 1; i < parameters.size(); i++) + { + std::string key, value; + if (SplitPair(key, value, parameters[i], '=') && + key == "q") + { + bool ok = false; + + try + { + quality = boost::lexical_cast(value); + ok = (quality >= 0.0f && quality <= 1.0f); + } + catch (boost::bad_lexical_cast&) + { + } + + if (ok) + { + assert(parameters.size() >= 2); + specificity = parameters.size() - 2; + return; + } + else + { + LOG(ERROR) << "Quality parameter out of range in a HTTP request (must be between 0 and 1): " << value; + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + } + } + + + static void SelectBestMatch(std::auto_ptr& best, + const Handler& handler, + const std::string& type, + const std::string& subtype, + float quality, + size_t specificity) + { + std::auto_ptr match(new Reference(handler, type, subtype, quality, specificity)); + + if (best.get() == NULL || + *best < *match) + { + best = match; + } + } + + + public: + void Register(const std::string& mime, + IHandler& handler) + { + std::string type, subtype; + + if (SplitPair(type, subtype, mime, '/') && + type != "*" && + subtype != "*") + { + handlers_.push_back(Handler(type, subtype, handler)); + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + bool Apply(const HttpHeaders& headers) + { + HttpHeaders::const_iterator accept = headers.find("accept"); + if (accept != headers.end()) + { + return Apply(accept->second); + } + else + { + return Apply("*/*"); + } + } + + + bool Apply(const std::string& accept) + { + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 + + Tokens mediaRanges; + Toolbox::TokenizeString(mediaRanges, accept, ','); + + std::auto_ptr bestMatch; + + for (Tokens::const_reverse_iterator it = mediaRanges.rbegin(); + it != mediaRanges.rend(); ++it) + { + Tokens parameters; + Toolbox::TokenizeString(parameters, *it, ';'); + + if (parameters.size() > 0) + { + float quality; + size_t specificity; + GetQualityAndSpecificity(quality, specificity, parameters); + + std::string type, subtype; + if (SplitPair(type, subtype, parameters[0], '/')) + { + for (Handlers::const_iterator it2 = handlers_.begin(); + it2 != handlers_.end(); ++it2) + { + if (it2->IsMatch(type, subtype)) + { + SelectBestMatch(bestMatch, *it2, type, subtype, quality, specificity); + } + } + } + } + } + + if (bestMatch.get() == NULL) // No match was found + { + return false; + } + else + { + bestMatch->Call(); + return true; + } + } + }; +} + + + +namespace +{ + class AcceptHandler : public Orthanc::AcceptMediaDispatcher::IHandler + { + private: + std::string type_; + std::string subtype_; + float quality_; + + public: + AcceptHandler() + { + Reset(); + } + + void Reset() + { + Handle("nope", "nope", 0.0f); + } + + const std::string& GetType() const + { + return type_; + } + + const std::string& GetSubType() const + { + return subtype_; + } + + float GetQuality() const + { + return quality_; + } + + virtual void Handle(const std::string& type, + const std::string& subtype, + float quality /* between 0 and 1 */) + { + type_ = type; + subtype_ = subtype; + quality_ = quality; + } + }; +} + + +TEST(RestApi, AcceptMediaDispatcher) +{ + // Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 + + AcceptHandler h; + + { + Orthanc::AcceptMediaDispatcher d; + d.Register("audio/mp3", h); + d.Register("audio/basic", h); + + ASSERT_TRUE(d.Apply("audio/*; q=0.2, audio/basic")); + ASSERT_EQ("audio", h.GetType()); + ASSERT_EQ("basic", h.GetSubType()); + ASSERT_FLOAT_EQ(1.0f, h.GetQuality()); + + ASSERT_TRUE(d.Apply("audio/*; q=0.2, audio/nope")); + ASSERT_EQ("audio", h.GetType()); + ASSERT_EQ("mp3", h.GetSubType()); + ASSERT_FLOAT_EQ(0.2f, h.GetQuality()); + + ASSERT_FALSE(d.Apply("application/*; q=0.2, application/pdf")); + + ASSERT_TRUE(d.Apply("*/*; application/*; q=0.2, application/pdf")); + ASSERT_EQ("audio", h.GetType()); + } + + // "This would be interpreted as "text/html and text/x-c are the + // preferred media types, but if they do not exist, then send the + // text/x-dvi entity, and if that does not exist, send the + // text/plain entity."" + const std::string T1 = "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"; + + { + Orthanc::AcceptMediaDispatcher d; + d.Register("text/plain", h); + //d.Register("text/x-dvi", h); + d.Register("text/html", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_EQ("html", h.GetSubType()); + ASSERT_EQ(1.0f, h.GetQuality()); + } + + { + Orthanc::AcceptMediaDispatcher d; + d.Register("text/plain", h); + //d.Register("text/x-dvi", h); + d.Register("text/x-c", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_EQ("x-c", h.GetSubType()); + ASSERT_EQ(1.0f, h.GetQuality()); + } + + { + Orthanc::AcceptMediaDispatcher d; + d.Register("text/plain", h); + d.Register("text/x-dvi", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_EQ("x-dvi", h.GetSubType()); + ASSERT_EQ(0.8f, h.GetQuality()); + } + + { + Orthanc::AcceptMediaDispatcher d; + d.Register("text/plain", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_EQ("plain", h.GetSubType()); + ASSERT_EQ(0.5f, h.GetQuality()); + } +}