Mercurial > hg > orthanc
diff OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp @ 4044:d25f4c0fa160 framework
splitting code into OrthancFramework and OrthancServer
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 10 Jun 2020 20:30:34 +0200 |
parents | OrthancServer/OrthancRestApi/OrthancRestResources.cpp@d86bddb50972 |
children | 05b8fd21089c |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed Jun 10 20:30:34 2020 +0200 @@ -0,0 +1,2165 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "../PrecompiledHeadersServer.h" +#include "OrthancRestApi.h" + +#include "../../Core/Compression/GzipCompressor.h" +#include "../../Core/DicomFormat/DicomImageInformation.h" +#include "../../Core/DicomParsing/DicomWebJsonVisitor.h" +#include "../../Core/DicomParsing/FromDcmtkBridge.h" +#include "../../Core/DicomParsing/Internals/DicomImageDecoder.h" +#include "../../Core/HttpServer/HttpContentNegociation.h" +#include "../../Core/Images/Image.h" +#include "../../Core/Images/ImageProcessing.h" +#include "../../Core/Logging.h" +#include "../../Core/MultiThreading/Semaphore.h" +#include "../OrthancConfiguration.h" +#include "../Search/DatabaseLookup.h" +#include "../ServerContext.h" +#include "../ServerToolbox.h" +#include "../SliceOrdering.h" + +#include "../../Plugins/Engine/OrthancPlugins.h" + +// This "include" is mandatory for Release builds using Linux Standard Base +#include <boost/math/special_functions/round.hpp> + + +/** + * This semaphore is used to limit the number of concurrent HTTP + * requests on CPU-intensive routes of the REST API, in order to + * prevent exhaustion of resources (new in Orthanc 1.7.0). + **/ +static Orthanc::Semaphore throttlingSemaphore_(4); // TODO => PARAMETER? + + +namespace Orthanc +{ + static void AnswerDicomAsJson(RestApiCall& call, + const Json::Value& dicom, + DicomToJsonFormat mode) + { + if (mode != DicomToJsonFormat_Full) + { + Json::Value simplified; + ServerToolbox::SimplifyTags(simplified, dicom, mode); + call.GetOutput().AnswerJson(simplified); + } + else + { + call.GetOutput().AnswerJson(dicom); + } + } + + + static DicomToJsonFormat GetDicomFormat(const RestApiGetCall& call) + { + if (call.HasArgument("simplify")) + { + return DicomToJsonFormat_Human; + } + else if (call.HasArgument("short")) + { + return DicomToJsonFormat_Short; + } + else + { + return DicomToJsonFormat_Full; + } + } + + + static void AnswerDicomAsJson(RestApiGetCall& call, + const Json::Value& dicom) + { + AnswerDicomAsJson(call, dicom, GetDicomFormat(call)); + } + + + static void ParseSetOfTags(std::set<DicomTag>& target, + const RestApiGetCall& call, + const std::string& argument) + { + target.clear(); + + if (call.HasArgument(argument)) + { + std::vector<std::string> tags; + Toolbox::TokenizeString(tags, call.GetArgument(argument, ""), ','); + + for (size_t i = 0; i < tags.size(); i++) + { + target.insert(FromDcmtkBridge::ParseTag(tags[i])); + } + } + } + + + // List all the patients, studies, series or instances ---------------------- + + static void AnswerListOfResources(RestApiOutput& output, + ServerIndex& index, + const std::list<std::string>& resources, + ResourceType level, + bool expand) + { + Json::Value answer = Json::arrayValue; + + for (std::list<std::string>::const_iterator + resource = resources.begin(); resource != resources.end(); ++resource) + { + if (expand) + { + Json::Value item; + if (index.LookupResource(item, *resource, level)) + { + answer.append(item); + } + } + else + { + answer.append(*resource); + } + } + + output.AnswerJson(answer); + } + + + template <enum ResourceType resourceType> + static void ListResources(RestApiGetCall& call) + { + ServerIndex& index = OrthancRestApi::GetIndex(call); + + std::list<std::string> result; + + if (call.HasArgument("limit") || + call.HasArgument("since")) + { + if (!call.HasArgument("limit")) + { + throw OrthancException(ErrorCode_BadRequest, + "Missing \"limit\" argument for GET request against: " + + call.FlattenUri()); + } + + if (!call.HasArgument("since")) + { + throw OrthancException(ErrorCode_BadRequest, + "Missing \"since\" argument for GET request against: " + + call.FlattenUri()); + } + + size_t since = boost::lexical_cast<size_t>(call.GetArgument("since", "")); + size_t limit = boost::lexical_cast<size_t>(call.GetArgument("limit", "")); + index.GetAllUuids(result, resourceType, since, limit); + } + else + { + index.GetAllUuids(result, resourceType); + } + + + AnswerListOfResources(call.GetOutput(), index, result, resourceType, call.HasArgument("expand")); + } + + template <enum ResourceType resourceType> + static void GetSingleResource(RestApiGetCall& call) + { + Json::Value result; + if (OrthancRestApi::GetIndex(call).LookupResource(result, call.GetUriComponent("id", ""), resourceType)) + { + call.GetOutput().AnswerJson(result); + } + } + + template <enum ResourceType resourceType> + static void DeleteSingleResource(RestApiDeleteCall& call) + { + Json::Value result; + if (OrthancRestApi::GetContext(call).DeleteResource(result, call.GetUriComponent("id", ""), resourceType)) + { + call.GetOutput().AnswerJson(result); + } + } + + + // Get information about a single patient ----------------------------------- + + static void IsProtectedPatient(RestApiGetCall& call) + { + std::string publicId = call.GetUriComponent("id", ""); + bool isProtected = OrthancRestApi::GetIndex(call).IsProtectedPatient(publicId); + call.GetOutput().AnswerBuffer(isProtected ? "1" : "0", MimeType_PlainText); + } + + + static void SetPatientProtection(RestApiPutCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string publicId = call.GetUriComponent("id", ""); + + std::string body; + call.BodyToString(body); + body = Toolbox::StripSpaces(body); + + if (body == "0") + { + context.GetIndex().SetProtectedPatient(publicId, false); + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + else if (body == "1") + { + context.GetIndex().SetProtectedPatient(publicId, true); + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + else + { + // Bad request + } + } + + + // Get information about a single instance ---------------------------------- + + static void GetInstanceFile(RestApiGetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string publicId = call.GetUriComponent("id", ""); + + IHttpHandler::Arguments::const_iterator accept = call.GetHttpHeaders().find("accept"); + if (accept != call.GetHttpHeaders().end()) + { + // New in Orthanc 1.5.4 + try + { + MimeType mime = StringToMimeType(accept->second.c_str()); + + if (mime == MimeType_DicomWebJson || + mime == MimeType_DicomWebXml) + { + DicomWebJsonVisitor visitor; + + { + ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId); + locker.GetDicom().Apply(visitor); + } + + if (mime == MimeType_DicomWebJson) + { + std::string s = visitor.GetResult().toStyledString(); + call.GetOutput().AnswerBuffer(s, MimeType_DicomWebJson); + } + else + { + std::string xml; + visitor.FormatXml(xml); + call.GetOutput().AnswerBuffer(xml, MimeType_DicomWebXml); + } + + return; + } + } + catch (OrthancException&) + { + } + } + + context.AnswerAttachment(call.GetOutput(), publicId, FileContentType_Dicom); + } + + + static void ExportInstanceFile(RestApiPostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string publicId = call.GetUriComponent("id", ""); + + std::string dicom; + context.ReadDicom(dicom, publicId); + + std::string target; + call.BodyToString(target); + SystemToolbox::WriteFile(dicom, target); + + call.GetOutput().AnswerBuffer("{}", MimeType_Json); + } + + + template <DicomToJsonFormat format> + static void GetInstanceTags(RestApiGetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string publicId = call.GetUriComponent("id", ""); + + std::set<DicomTag> ignoreTagLength; + ParseSetOfTags(ignoreTagLength, call, "ignore-length"); + + if (format != DicomToJsonFormat_Full || + !ignoreTagLength.empty()) + { + Json::Value full; + context.ReadDicomAsJson(full, publicId, ignoreTagLength); + AnswerDicomAsJson(call, full, format); + } + else + { + // This path allows one to avoid the JSON decoding if no + // simplification is asked, and if no "ignore-length" argument + // is present + std::string full; + context.ReadDicomAsJson(full, publicId); + call.GetOutput().AnswerBuffer(full, MimeType_Json); + } + } + + + static void GetInstanceTagsBis(RestApiGetCall& call) + { + switch (GetDicomFormat(call)) + { + case DicomToJsonFormat_Human: + GetInstanceTags<DicomToJsonFormat_Human>(call); + break; + + case DicomToJsonFormat_Short: + GetInstanceTags<DicomToJsonFormat_Short>(call); + break; + + case DicomToJsonFormat_Full: + GetInstanceTags<DicomToJsonFormat_Full>(call); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + static void ListFrames(RestApiGetCall& call) + { + std::string publicId = call.GetUriComponent("id", ""); + + unsigned int numberOfFrames; + + { + ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId); + numberOfFrames = locker.GetDicom().GetFramesCount(); + } + + Json::Value result = Json::arrayValue; + for (unsigned int i = 0; i < numberOfFrames; i++) + { + result.append(i); + } + + call.GetOutput().AnswerJson(result); + } + + + namespace + { + class ImageToEncode + { + private: + std::unique_ptr<ImageAccessor>& image_; + ImageExtractionMode mode_; + bool invert_; + MimeType format_; + std::string answer_; + + public: + ImageToEncode(std::unique_ptr<ImageAccessor>& image, + ImageExtractionMode mode, + bool invert) : + image_(image), + mode_(mode), + invert_(invert) + { + } + + void Answer(RestApiOutput& output) + { + output.AnswerBuffer(answer_, format_); + } + + void EncodeUsingPng() + { + format_ = MimeType_Png; + DicomImageDecoder::ExtractPngImage(answer_, image_, mode_, invert_); + } + + void EncodeUsingPam() + { + format_ = MimeType_Pam; + DicomImageDecoder::ExtractPamImage(answer_, image_, mode_, invert_); + } + + void EncodeUsingJpeg(uint8_t quality) + { + format_ = MimeType_Jpeg; + DicomImageDecoder::ExtractJpegImage(answer_, image_, mode_, invert_, quality); + } + }; + + class EncodePng : public HttpContentNegociation::IHandler + { + private: + ImageToEncode& image_; + + public: + EncodePng(ImageToEncode& image) : image_(image) + { + } + + virtual void Handle(const std::string& type, + const std::string& subtype) + { + assert(type == "image"); + assert(subtype == "png"); + image_.EncodeUsingPng(); + } + }; + + 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 == "x-portable-arbitrarymap"); + image_.EncodeUsingPam(); + } + }; + + class EncodeJpeg : public HttpContentNegociation::IHandler + { + private: + ImageToEncode& image_; + unsigned int quality_; + + public: + EncodeJpeg(ImageToEncode& image, + const RestApiGetCall& call) : + image_(image) + { + std::string v = call.GetArgument("quality", "90" /* default JPEG quality */); + bool ok = false; + + try + { + quality_ = boost::lexical_cast<unsigned int>(v); + ok = (quality_ >= 1 && quality_ <= 100); + } + catch (boost::bad_lexical_cast&) + { + } + + if (!ok) + { + throw OrthancException( + ErrorCode_BadRequest, + "Bad quality for a JPEG encoding (must be a number between 0 and 100): " + v); + } + } + + virtual void Handle(const std::string& type, + const std::string& subtype) + { + assert(type == "image"); + assert(subtype == "jpeg"); + image_.EncodeUsingJpeg(quality_); + } + }; + } + + + namespace + { + class IDecodedFrameHandler : public boost::noncopyable + { + public: + virtual ~IDecodedFrameHandler() + { + } + + virtual void Handle(RestApiGetCall& call, + std::unique_ptr<ImageAccessor>& decoded, + const DicomMap& dicom) = 0; + + virtual bool RequiresDicomTags() const = 0; + + static void Apply(RestApiGetCall& call, + IDecodedFrameHandler& handler) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string frameId = call.GetUriComponent("frame", "0"); + + unsigned int frame; + try + { + frame = boost::lexical_cast<unsigned int>(frameId); + } + catch (boost::bad_lexical_cast&) + { + return; + } + + DicomMap dicom; + std::unique_ptr<ImageAccessor> decoded; + + try + { + std::string publicId = call.GetUriComponent("id", ""); + + decoded.reset(context.DecodeDicomFrame(publicId, frame)); + + if (decoded.get() == NULL) + { + throw OrthancException(ErrorCode_NotImplemented, + "Cannot decode DICOM instance with ID: " + publicId); + } + + if (handler.RequiresDicomTags()) + { + /** + * Retrieve a summary of the DICOM tags, which is + * necessary to deal with MONOCHROME1 photometric + * interpretation, and with windowing parameters. + **/ + ServerContext::DicomCacheLocker locker(context, publicId); + locker.GetDicom().ExtractDicomSummary(dicom); + } + } + catch (OrthancException& e) + { + if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange || + e.GetErrorCode() == ErrorCode_UnknownResource) + { + // The frame number is out of the range for this DICOM + // instance, the resource is not existent + } + else + { + std::string root = ""; + for (size_t i = 1; i < call.GetFullUri().size(); i++) + { + root += "../"; + } + + call.GetOutput().Redirect(root + "app/images/unsupported.png"); + } + return; + } + + handler.Handle(call, decoded, dicom); + } + + + static void DefaultHandler(RestApiGetCall& call, + std::unique_ptr<ImageAccessor>& decoded, + ImageExtractionMode mode, + bool invert) + { + ImageToEncode image(decoded, mode, invert); + + HttpContentNegociation negociation; + EncodePng png(image); + negociation.Register(MIME_PNG, png); + + EncodeJpeg jpeg(image, call); + negociation.Register(MIME_JPEG, jpeg); + + EncodePam pam(image); + negociation.Register(MIME_PAM, pam); + + if (negociation.Apply(call.GetHttpHeaders())) + { + image.Answer(call.GetOutput()); + } + } + }; + + + class GetImageHandler : public IDecodedFrameHandler + { + private: + ImageExtractionMode mode_; + + public: + GetImageHandler(ImageExtractionMode mode) : + mode_(mode) + { + } + + virtual void Handle(RestApiGetCall& call, + std::unique_ptr<ImageAccessor>& decoded, + const DicomMap& dicom) ORTHANC_OVERRIDE + { + bool invert = false; + + if (mode_ == ImageExtractionMode_Preview) + { + DicomImageInformation info(dicom); + invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1); + } + + DefaultHandler(call, decoded, mode_, invert); + } + + virtual bool RequiresDicomTags() const ORTHANC_OVERRIDE + { + return mode_ == ImageExtractionMode_Preview; + } + }; + + + class RenderedFrameHandler : public IDecodedFrameHandler + { + private: + static void GetDicomParameters(bool& invert, + float& rescaleSlope, + float& rescaleIntercept, + float& windowWidth, + float& windowCenter, + const DicomMap& dicom) + { + DicomImageInformation info(dicom); + + invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1); + + rescaleSlope = 1.0f; + rescaleIntercept = 0.0f; + + if (dicom.HasTag(Orthanc::DICOM_TAG_RESCALE_SLOPE) && + dicom.HasTag(Orthanc::DICOM_TAG_RESCALE_INTERCEPT)) + { + dicom.ParseFloat(rescaleSlope, Orthanc::DICOM_TAG_RESCALE_SLOPE); + dicom.ParseFloat(rescaleIntercept, Orthanc::DICOM_TAG_RESCALE_INTERCEPT); + } + + windowWidth = static_cast<float>(1 << info.GetBitsStored()) * rescaleSlope; + windowCenter = windowWidth / 2.0f + rescaleIntercept; + + if (dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_CENTER) && + dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_WIDTH)) + { + dicom.ParseFirstFloat(windowCenter, Orthanc::DICOM_TAG_WINDOW_CENTER); + dicom.ParseFirstFloat(windowWidth, Orthanc::DICOM_TAG_WINDOW_WIDTH); + } + } + + static void GetUserArguments(float& windowWidth /* inout */, + float& windowCenter /* inout */, + unsigned int& argWidth, + unsigned int& argHeight, + bool& smooth, + RestApiGetCall& call) + { + static const char* ARG_WINDOW_CENTER = "window-center"; + static const char* ARG_WINDOW_WIDTH = "window-width"; + static const char* ARG_WIDTH = "width"; + static const char* ARG_HEIGHT = "height"; + static const char* ARG_SMOOTH = "smooth"; + + if (call.HasArgument(ARG_WINDOW_WIDTH)) + { + try + { + windowWidth = boost::lexical_cast<float>(call.GetArgument(ARG_WINDOW_WIDTH, "")); + } + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Bad value for argument: " + std::string(ARG_WINDOW_WIDTH)); + } + } + + if (call.HasArgument(ARG_WINDOW_CENTER)) + { + try + { + windowCenter = boost::lexical_cast<float>(call.GetArgument(ARG_WINDOW_CENTER, "")); + } + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Bad value for argument: " + std::string(ARG_WINDOW_CENTER)); + } + } + + argWidth = 0; + argHeight = 0; + + if (call.HasArgument(ARG_WIDTH)) + { + try + { + int tmp = boost::lexical_cast<int>(call.GetArgument(ARG_WIDTH, "")); + if (tmp < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Argument cannot be negative: " + std::string(ARG_WIDTH)); + } + else + { + argWidth = static_cast<unsigned int>(tmp); + } + } + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Bad value for argument: " + std::string(ARG_WIDTH)); + } + } + + if (call.HasArgument(ARG_HEIGHT)) + { + try + { + int tmp = boost::lexical_cast<int>(call.GetArgument(ARG_HEIGHT, "")); + if (tmp < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Argument cannot be negative: " + std::string(ARG_HEIGHT)); + } + else + { + argHeight = static_cast<unsigned int>(tmp); + } + } + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Bad value for argument: " + std::string(ARG_HEIGHT)); + } + } + + smooth = false; + + if (call.HasArgument(ARG_SMOOTH)) + { + std::string value = call.GetArgument(ARG_SMOOTH, ""); + if (value == "0" || + value == "false") + { + smooth = false; + } + else if (value == "1" || + value == "true") + { + smooth = true; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Argument must be Boolean: " + std::string(ARG_SMOOTH)); + } + } + } + + + public: + virtual void Handle(RestApiGetCall& call, + std::unique_ptr<ImageAccessor>& decoded, + const DicomMap& dicom) ORTHANC_OVERRIDE + { + bool invert; + float rescaleSlope, rescaleIntercept, windowWidth, windowCenter; + GetDicomParameters(invert, rescaleSlope, rescaleIntercept, windowWidth, windowCenter, dicom); + + unsigned int argWidth, argHeight; + bool smooth; + GetUserArguments(windowWidth, windowCenter, argWidth, argHeight, smooth, call); + + unsigned int targetWidth = decoded->GetWidth(); + unsigned int targetHeight = decoded->GetHeight(); + + if (decoded->GetWidth() != 0 && + decoded->GetHeight() != 0) + { + float ratio = 1; + + if (argWidth != 0 && + argHeight != 0) + { + float ratioX = static_cast<float>(argWidth) / static_cast<float>(decoded->GetWidth()); + float ratioY = static_cast<float>(argHeight) / static_cast<float>(decoded->GetHeight()); + ratio = std::min(ratioX, ratioY); + } + else if (argWidth != 0) + { + ratio = static_cast<float>(argWidth) / static_cast<float>(decoded->GetWidth()); + } + else if (argHeight != 0) + { + ratio = static_cast<float>(argHeight) / static_cast<float>(decoded->GetHeight()); + } + + targetWidth = boost::math::iround(ratio * static_cast<float>(decoded->GetWidth())); + targetHeight = boost::math::iround(ratio * static_cast<float>(decoded->GetHeight())); + } + + if (decoded->GetFormat() == PixelFormat_RGB24) + { + if (targetWidth == decoded->GetWidth() && + targetHeight == decoded->GetHeight()) + { + DefaultHandler(call, decoded, ImageExtractionMode_Preview, false); + } + else + { + std::unique_ptr<ImageAccessor> resized( + new Image(decoded->GetFormat(), targetWidth, targetHeight, false)); + + if (smooth && + (targetWidth < decoded->GetWidth() || + targetHeight < decoded->GetHeight())) + { + ImageProcessing::SmoothGaussian5x5(*decoded); + } + + ImageProcessing::Resize(*resized, *decoded); + DefaultHandler(call, resized, ImageExtractionMode_Preview, false); + } + } + else + { + // Grayscale image: (1) convert to Float32, (2) apply + // windowing to get a Grayscale8, (3) possibly resize + + Image converted(PixelFormat_Float32, decoded->GetWidth(), decoded->GetHeight(), false); + ImageProcessing::Convert(converted, *decoded); + + // Avoid divisions by zero + if (windowWidth <= 1.0f) + { + windowWidth = 1; + } + + if (std::abs(rescaleSlope) <= 0.1f) + { + rescaleSlope = 0.1f; + } + + const float scaling = 255.0f * rescaleSlope / windowWidth; + const float offset = (rescaleIntercept - windowCenter + windowWidth / 2.0f) / rescaleSlope; + + std::unique_ptr<ImageAccessor> rescaled(new Image(PixelFormat_Grayscale8, decoded->GetWidth(), decoded->GetHeight(), false)); + ImageProcessing::ShiftScale(*rescaled, converted, offset, scaling, false); + + if (targetWidth == decoded->GetWidth() && + targetHeight == decoded->GetHeight()) + { + DefaultHandler(call, rescaled, ImageExtractionMode_UInt8, invert); + } + else + { + std::unique_ptr<ImageAccessor> resized( + new Image(PixelFormat_Grayscale8, targetWidth, targetHeight, false)); + + if (smooth && + (targetWidth < decoded->GetWidth() || + targetHeight < decoded->GetHeight())) + { + ImageProcessing::SmoothGaussian5x5(*rescaled); + } + + ImageProcessing::Resize(*resized, *rescaled); + DefaultHandler(call, resized, ImageExtractionMode_UInt8, invert); + } + } + } + + virtual bool RequiresDicomTags() const ORTHANC_OVERRIDE + { + return true; + } + }; + } + + + template <enum ImageExtractionMode mode> + static void GetImage(RestApiGetCall& call) + { + Semaphore::Locker locker(throttlingSemaphore_); + + GetImageHandler handler(mode); + IDecodedFrameHandler::Apply(call, handler); + } + + + static void GetRenderedFrame(RestApiGetCall& call) + { + Semaphore::Locker locker(throttlingSemaphore_); + + RenderedFrameHandler handler; + IDecodedFrameHandler::Apply(call, handler); + } + + + static void GetMatlabImage(RestApiGetCall& call) + { + Semaphore::Locker locker(throttlingSemaphore_); + + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string frameId = call.GetUriComponent("frame", "0"); + + unsigned int frame; + try + { + frame = boost::lexical_cast<unsigned int>(frameId); + } + catch (boost::bad_lexical_cast&) + { + return; + } + + std::string publicId = call.GetUriComponent("id", ""); + std::unique_ptr<ImageAccessor> decoded(context.DecodeDicomFrame(publicId, frame)); + + if (decoded.get() == NULL) + { + throw OrthancException(ErrorCode_NotImplemented, + "Cannot decode DICOM instance with ID: " + publicId); + } + else + { + std::string result; + decoded->ToMatlabString(result); + call.GetOutput().AnswerBuffer(result, MimeType_PlainText); + } + } + + + template <bool GzipCompression> + static void GetRawFrame(RestApiGetCall& call) + { + std::string frameId = call.GetUriComponent("frame", "0"); + + unsigned int frame; + try + { + frame = boost::lexical_cast<unsigned int>(frameId); + } + catch (boost::bad_lexical_cast&) + { + return; + } + + std::string publicId = call.GetUriComponent("id", ""); + std::string raw; + MimeType mime; + + { + ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId); + locker.GetDicom().GetRawFrame(raw, mime, frame); + } + + if (GzipCompression) + { + GzipCompressor gzip; + std::string compressed; + gzip.Compress(compressed, raw.empty() ? NULL : raw.c_str(), raw.size()); + call.GetOutput().AnswerBuffer(compressed, MimeType_Gzip); + } + else + { + call.GetOutput().AnswerBuffer(raw, mime); + } + } + + + static void GetResourceStatistics(RestApiGetCall& call) + { + static const uint64_t MEGA_BYTES = 1024 * 1024; + + std::string publicId = call.GetUriComponent("id", ""); + + ResourceType type; + uint64_t diskSize, uncompressedSize, dicomDiskSize, dicomUncompressedSize; + unsigned int countStudies, countSeries, countInstances; + OrthancRestApi::GetIndex(call).GetResourceStatistics( + type, diskSize, uncompressedSize, countStudies, countSeries, + countInstances, dicomDiskSize, dicomUncompressedSize, publicId); + + Json::Value result = Json::objectValue; + result["DiskSize"] = boost::lexical_cast<std::string>(diskSize); + result["DiskSizeMB"] = static_cast<unsigned int>(diskSize / MEGA_BYTES); + result["UncompressedSize"] = boost::lexical_cast<std::string>(uncompressedSize); + result["UncompressedSizeMB"] = static_cast<unsigned int>(uncompressedSize / MEGA_BYTES); + + result["DicomDiskSize"] = boost::lexical_cast<std::string>(dicomDiskSize); + result["DicomDiskSizeMB"] = static_cast<unsigned int>(dicomDiskSize / MEGA_BYTES); + result["DicomUncompressedSize"] = boost::lexical_cast<std::string>(dicomUncompressedSize); + result["DicomUncompressedSizeMB"] = static_cast<unsigned int>(dicomUncompressedSize / MEGA_BYTES); + + switch (type) + { + // Do NOT add "break" below this point! + case ResourceType_Patient: + result["CountStudies"] = countStudies; + + case ResourceType_Study: + result["CountSeries"] = countSeries; + + case ResourceType_Series: + result["CountInstances"] = countInstances; + + case ResourceType_Instance: + default: + break; + } + + call.GetOutput().AnswerJson(result); + } + + + + // Handling of metadata ----------------------------------------------------- + + static void CheckValidResourceType(RestApiCall& call) + { + std::string resourceType = call.GetUriComponent("resourceType", ""); + StringToResourceType(resourceType.c_str()); + } + + + static void ListMetadata(RestApiGetCall& call) + { + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::map<MetadataType, std::string> metadata; + + OrthancRestApi::GetIndex(call).GetAllMetadata(metadata, publicId); + + Json::Value result; + + if (call.HasArgument("expand")) + { + result = Json::objectValue; + + for (std::map<MetadataType, std::string>::const_iterator + it = metadata.begin(); it != metadata.end(); ++it) + { + std::string key = EnumerationToString(it->first); + result[key] = it->second; + } + } + else + { + result = Json::arrayValue; + + for (std::map<MetadataType, std::string>::const_iterator + it = metadata.begin(); it != metadata.end(); ++it) + { + result.append(EnumerationToString(it->first)); + } + } + + call.GetOutput().AnswerJson(result); + } + + + static void GetMetadata(RestApiGetCall& call) + { + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::string name = call.GetUriComponent("name", ""); + MetadataType metadata = StringToMetadata(name); + + std::string value; + if (OrthancRestApi::GetIndex(call).LookupMetadata(value, publicId, metadata)) + { + call.GetOutput().AnswerBuffer(value, MimeType_PlainText); + } + } + + + static void DeleteMetadata(RestApiDeleteCall& call) + { + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::string name = call.GetUriComponent("name", ""); + MetadataType metadata = StringToMetadata(name); + + if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata + { + OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata); + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + else + { + call.GetOutput().SignalError(HttpStatus_403_Forbidden); + } + } + + + static void SetMetadata(RestApiPutCall& call) + { + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::string name = call.GetUriComponent("name", ""); + MetadataType metadata = StringToMetadata(name); + + std::string value; + call.BodyToString(value); + + if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata + { + // It is forbidden to modify internal metadata + OrthancRestApi::GetIndex(call).SetMetadata(publicId, metadata, value); + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + else + { + call.GetOutput().SignalError(HttpStatus_403_Forbidden); + } + } + + + + + // Handling of attached files ----------------------------------------------- + + static void ListAttachments(RestApiGetCall& call) + { + std::string resourceType = call.GetUriComponent("resourceType", ""); + std::string publicId = call.GetUriComponent("id", ""); + std::list<FileContentType> attachments; + OrthancRestApi::GetIndex(call).ListAvailableAttachments(attachments, publicId, StringToResourceType(resourceType.c_str())); + + Json::Value result = Json::arrayValue; + + for (std::list<FileContentType>::const_iterator + it = attachments.begin(); it != attachments.end(); ++it) + { + result.append(EnumerationToString(*it)); + } + + call.GetOutput().AnswerJson(result); + } + + + static bool GetAttachmentInfo(FileInfo& info, RestApiCall& call) + { + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::string name = call.GetUriComponent("name", ""); + FileContentType contentType = StringToContentType(name); + + return OrthancRestApi::GetIndex(call).LookupAttachment(info, publicId, contentType); + } + + + static void GetAttachmentOperations(RestApiGetCall& call) + { + FileInfo info; + if (GetAttachmentInfo(info, call)) + { + Json::Value operations = Json::arrayValue; + + operations.append("compress"); + operations.append("compressed-data"); + + if (info.GetCompressedMD5() != "") + { + operations.append("compressed-md5"); + } + + operations.append("compressed-size"); + operations.append("data"); + operations.append("is-compressed"); + + if (info.GetUncompressedMD5() != "") + { + operations.append("md5"); + } + + operations.append("size"); + operations.append("uncompress"); + + if (info.GetCompressedMD5() != "" && + info.GetUncompressedMD5() != "") + { + operations.append("verify-md5"); + } + + call.GetOutput().AnswerJson(operations); + } + } + + + template <int uncompress> + static void GetAttachmentData(RestApiGetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + FileContentType type = StringToContentType(call.GetUriComponent("name", "")); + + if (uncompress) + { + context.AnswerAttachment(call.GetOutput(), publicId, type); + } + else + { + // Return the raw data (possibly compressed), as stored on the filesystem + std::string content; + context.ReadAttachment(content, publicId, type, false); + call.GetOutput().AnswerBuffer(content, MimeType_Binary); + } + } + + + static void GetAttachmentSize(RestApiGetCall& call) + { + FileInfo info; + if (GetAttachmentInfo(info, call)) + { + call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedSize()), MimeType_PlainText); + } + } + + + static void GetAttachmentCompressedSize(RestApiGetCall& call) + { + FileInfo info; + if (GetAttachmentInfo(info, call)) + { + call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedSize()), MimeType_PlainText); + } + } + + + static void GetAttachmentMD5(RestApiGetCall& call) + { + FileInfo info; + if (GetAttachmentInfo(info, call) && + info.GetUncompressedMD5() != "") + { + call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedMD5()), MimeType_PlainText); + } + } + + + static void GetAttachmentCompressedMD5(RestApiGetCall& call) + { + FileInfo info; + if (GetAttachmentInfo(info, call) && + info.GetCompressedMD5() != "") + { + call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedMD5()), MimeType_PlainText); + } + } + + + static void VerifyAttachment(RestApiPostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::string name = call.GetUriComponent("name", ""); + + FileInfo info; + if (!GetAttachmentInfo(info, call) || + info.GetCompressedMD5() == "" || + info.GetUncompressedMD5() == "") + { + // Inexistent resource, or no MD5 available + return; + } + + bool ok = false; + + // First check whether the compressed data is correctly stored in the disk + std::string data; + context.ReadAttachment(data, publicId, StringToContentType(name), false); + + std::string actualMD5; + Toolbox::ComputeMD5(actualMD5, data); + + if (actualMD5 == info.GetCompressedMD5()) + { + // The compressed data is OK. If a compression algorithm was + // applied to it, now check the MD5 of the uncompressed data. + if (info.GetCompressionType() == CompressionType_None) + { + ok = true; + } + else + { + context.ReadAttachment(data, publicId, StringToContentType(name), true); + Toolbox::ComputeMD5(actualMD5, data); + ok = (actualMD5 == info.GetUncompressedMD5()); + } + } + + if (ok) + { + LOG(INFO) << "The attachment " << name << " of resource " << publicId << " has the right MD5"; + call.GetOutput().AnswerBuffer("{}", MimeType_Json); + } + else + { + LOG(INFO) << "The attachment " << name << " of resource " << publicId << " has bad MD5!"; + } + } + + + static void UploadAttachment(RestApiPutCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::string name = call.GetUriComponent("name", ""); + + FileContentType contentType = StringToContentType(name); + if (IsUserContentType(contentType) && // It is forbidden to modify internal attachments + context.AddAttachment(publicId, StringToContentType(name), call.GetBodyData(), call.GetBodySize())) + { + call.GetOutput().AnswerBuffer("{}", MimeType_Json); + } + else + { + call.GetOutput().SignalError(HttpStatus_403_Forbidden); + } + } + + + static void DeleteAttachment(RestApiDeleteCall& call) + { + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::string name = call.GetUriComponent("name", ""); + FileContentType contentType = StringToContentType(name); + + bool allowed; + if (IsUserContentType(contentType)) + { + allowed = true; + } + else + { + OrthancConfiguration::ReaderLock lock; + + if (lock.GetConfiguration().GetBooleanParameter("StoreDicom", true) && + contentType == FileContentType_DicomAsJson) + { + allowed = true; + } + else + { + // It is forbidden to delete internal attachments, except for + // the "DICOM as JSON" summary as of Orthanc 1.2.0 (this summary + // would be automatically reconstructed on the next GET call) + allowed = false; + } + } + + if (allowed) + { + OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType); + call.GetOutput().AnswerBuffer("{}", MimeType_Json); + } + else + { + call.GetOutput().SignalError(HttpStatus_403_Forbidden); + } + } + + + template <enum CompressionType compression> + static void ChangeAttachmentCompression(RestApiPostCall& call) + { + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + std::string name = call.GetUriComponent("name", ""); + FileContentType contentType = StringToContentType(name); + + OrthancRestApi::GetContext(call).ChangeAttachmentCompression(publicId, contentType, compression); + call.GetOutput().AnswerBuffer("{}", MimeType_Json); + } + + + static void IsAttachmentCompressed(RestApiGetCall& call) + { + FileInfo info; + if (GetAttachmentInfo(info, call)) + { + std::string answer = (info.GetCompressionType() == CompressionType_None) ? "0" : "1"; + call.GetOutput().AnswerBuffer(answer, MimeType_PlainText); + } + } + + + // Raw access to the DICOM tags of an instance ------------------------------ + + static void GetRawContent(RestApiGetCall& call) + { + std::string id = call.GetUriComponent("id", ""); + + ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), id); + + locker.GetDicom().SendPathValue(call.GetOutput(), call.GetTrailingUri()); + } + + + + static bool ExtractSharedTags(Json::Value& shared, + ServerContext& context, + const std::string& publicId) + { + // Retrieve all the instances of this patient/study/series + typedef std::list<std::string> Instances; + Instances instances; + context.GetIndex().GetChildInstances(instances, publicId); // (*) + + // Loop over the instances + bool isFirst = true; + shared = Json::objectValue; + + for (Instances::const_iterator it = instances.begin(); + it != instances.end(); ++it) + { + // Get the tags of the current instance, in the simplified format + Json::Value tags; + + try + { + context.ReadDicomAsJson(tags, *it); + } + catch (OrthancException&) + { + // Race condition: This instance has been removed since + // (*). Ignore this instance. + continue; + } + + if (tags.type() != Json::objectValue) + { + return false; // Error + } + + // Only keep the tags that are mapped to a string + Json::Value::Members members = tags.getMemberNames(); + for (size_t i = 0; i < members.size(); i++) + { + const Json::Value& tag = tags[members[i]]; + if (tag.type() != Json::objectValue || + tag["Type"].type() != Json::stringValue || + tag["Type"].asString() != "String") + { + tags.removeMember(members[i]); + } + } + + if (isFirst) + { + // This is the first instance, keep its tags as such + shared = tags; + isFirst = false; + } + else + { + // Loop over all the members of the shared tags extracted so + // far. If the value of one of these tags does not match its + // value in the current instance, remove it. + members = shared.getMemberNames(); + for (size_t i = 0; i < members.size(); i++) + { + if (!tags.isMember(members[i]) || + tags[members[i]]["Value"].asString() != shared[members[i]]["Value"].asString()) + { + shared.removeMember(members[i]); + } + } + } + } + + return true; + } + + + static void GetSharedTags(RestApiGetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + std::string publicId = call.GetUriComponent("id", ""); + + Json::Value sharedTags; + if (ExtractSharedTags(sharedTags, context, publicId)) + { + // Success: Send the value of the shared tags + AnswerDicomAsJson(call, sharedTags); + } + } + + + static void GetModuleInternal(RestApiGetCall& call, + ResourceType resourceType, + DicomModule module) + { + if (!((resourceType == ResourceType_Patient && module == DicomModule_Patient) || + (resourceType == ResourceType_Study && module == DicomModule_Patient) || + (resourceType == ResourceType_Study && module == DicomModule_Study) || + (resourceType == ResourceType_Series && module == DicomModule_Series) || + (resourceType == ResourceType_Instance && module == DicomModule_Instance) || + (resourceType == ResourceType_Instance && module == DicomModule_Image))) + { + throw OrthancException(ErrorCode_NotImplemented); + } + + ServerContext& context = OrthancRestApi::GetContext(call); + std::string publicId = call.GetUriComponent("id", ""); + + std::set<DicomTag> ignoreTagLength; + ParseSetOfTags(ignoreTagLength, call, "ignore-length"); + + typedef std::set<DicomTag> ModuleTags; + ModuleTags moduleTags; + DicomTag::AddTagsForModule(moduleTags, module); + + Json::Value tags; + + if (resourceType != ResourceType_Instance) + { + // Retrieve all the instances of this patient/study/series + typedef std::list<std::string> Instances; + Instances instances; + context.GetIndex().GetChildInstances(instances, publicId); + + if (instances.empty()) + { + return; // Error: No instance (should never happen) + } + + // Select one child instance + publicId = instances.front(); + } + + context.ReadDicomAsJson(tags, publicId, ignoreTagLength); + + // Filter the tags of the instance according to the module + Json::Value result = Json::objectValue; + for (ModuleTags::const_iterator tag = moduleTags.begin(); tag != moduleTags.end(); ++tag) + { + std::string s = tag->Format(); + if (tags.isMember(s)) + { + result[s] = tags[s]; + } + } + + AnswerDicomAsJson(call, result); + } + + + + template <enum ResourceType resourceType, + enum DicomModule module> + static void GetModule(RestApiGetCall& call) + { + GetModuleInternal(call, resourceType, module); + } + + + namespace + { + typedef std::list< std::pair<ResourceType, std::string> > LookupResults; + } + + + static void AccumulateLookupResults(LookupResults& result, + ServerIndex& index, + const DicomTag& tag, + const std::string& value, + ResourceType level) + { + std::vector<std::string> tmp; + index.LookupIdentifierExact(tmp, level, tag, value); + + for (size_t i = 0; i < tmp.size(); i++) + { + result.push_back(std::make_pair(level, tmp[i])); + } + } + + + static void Lookup(RestApiPostCall& call) + { + std::string tag; + call.BodyToString(tag); + + LookupResults resources; + ServerIndex& index = OrthancRestApi::GetIndex(call); + AccumulateLookupResults(resources, index, DICOM_TAG_PATIENT_ID, tag, ResourceType_Patient); + AccumulateLookupResults(resources, index, DICOM_TAG_STUDY_INSTANCE_UID, tag, ResourceType_Study); + AccumulateLookupResults(resources, index, DICOM_TAG_SERIES_INSTANCE_UID, tag, ResourceType_Series); + AccumulateLookupResults(resources, index, DICOM_TAG_SOP_INSTANCE_UID, tag, ResourceType_Instance); + + Json::Value result = Json::arrayValue; + for (LookupResults::const_iterator + it = resources.begin(); it != resources.end(); ++it) + { + ResourceType type = it->first; + const std::string& id = it->second; + + Json::Value item = Json::objectValue; + item["Type"] = EnumerationToString(type); + item["ID"] = id; + item["Path"] = GetBasePath(type, id); + + result.append(item); + } + + call.GetOutput().AnswerJson(result); + } + + + namespace + { + class FindVisitor : public ServerContext::ILookupVisitor + { + private: + bool isComplete_; + std::list<std::string> resources_; + + public: + FindVisitor() : + isComplete_(false) + { + } + + virtual bool IsDicomAsJsonNeeded() const + { + return false; // (*) + } + + virtual void MarkAsComplete() + { + isComplete_ = true; // Unused information as of Orthanc 1.5.0 + } + + virtual void Visit(const std::string& publicId, + const std::string& instanceId /* unused */, + const DicomMap& mainDicomTags /* unused */, + const Json::Value* dicomAsJson /* unused (*) */) + { + resources_.push_back(publicId); + } + + void Answer(RestApiOutput& output, + ServerIndex& index, + ResourceType level, + bool expand) const + { + AnswerListOfResources(output, index, resources_, level, expand); + } + }; + } + + + static void Find(RestApiPostCall& call) + { + static const char* const KEY_CASE_SENSITIVE = "CaseSensitive"; + static const char* const KEY_EXPAND = "Expand"; + static const char* const KEY_LEVEL = "Level"; + static const char* const KEY_LIMIT = "Limit"; + static const char* const KEY_QUERY = "Query"; + static const char* const KEY_SINCE = "Since"; + + ServerContext& context = OrthancRestApi::GetContext(call); + + Json::Value request; + if (!call.ParseJsonRequest(request) || + request.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadRequest, + "The body must contain a JSON object"); + } + else if (!request.isMember(KEY_LEVEL) || + request[KEY_LEVEL].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_LEVEL) + "\" is missing, or should be a string"); + } + else if (!request.isMember(KEY_QUERY) && + request[KEY_QUERY].type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_QUERY) + "\" is missing, or should be a JSON object"); + } + else if (request.isMember(KEY_CASE_SENSITIVE) && + request[KEY_CASE_SENSITIVE].type() != Json::booleanValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_CASE_SENSITIVE) + "\" should be a Boolean"); + } + else if (request.isMember(KEY_LIMIT) && + request[KEY_LIMIT].type() != Json::intValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_LIMIT) + "\" should be an integer"); + } + else if (request.isMember(KEY_SINCE) && + request[KEY_SINCE].type() != Json::intValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_SINCE) + "\" should be an integer"); + } + else + { + bool expand = false; + if (request.isMember(KEY_EXPAND)) + { + expand = request[KEY_EXPAND].asBool(); + } + + bool caseSensitive = false; + if (request.isMember(KEY_CASE_SENSITIVE)) + { + caseSensitive = request[KEY_CASE_SENSITIVE].asBool(); + } + + size_t limit = 0; + if (request.isMember(KEY_LIMIT)) + { + int tmp = request[KEY_LIMIT].asInt(); + if (tmp < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Field \"" + std::string(KEY_LIMIT) + "\" should be a positive integer"); + } + + limit = static_cast<size_t>(tmp); + } + + size_t since = 0; + if (request.isMember(KEY_SINCE)) + { + int tmp = request[KEY_SINCE].asInt(); + if (tmp < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Field \"" + std::string(KEY_SINCE) + "\" should be a positive integer"); + } + + since = static_cast<size_t>(tmp); + } + + ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString()); + + DatabaseLookup query; + + Json::Value::Members members = request[KEY_QUERY].getMemberNames(); + for (size_t i = 0; i < members.size(); i++) + { + if (request[KEY_QUERY][members[i]].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Tag \"" + members[i] + "\" should be associated with a string"); + } + + const std::string value = request[KEY_QUERY][members[i]].asString(); + + if (!value.empty()) + { + // An empty string corresponds to an universal constraint, + // so we ignore it. This mimics the behavior of class + // "OrthancFindRequestHandler" + query.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]), + value, caseSensitive, true); + } + } + + FindVisitor visitor; + context.Apply(visitor, query, level, since, limit); + visitor.Answer(call.GetOutput(), context.GetIndex(), level, expand); + } + } + + + template <enum ResourceType start, + enum ResourceType end> + static void GetChildResources(RestApiGetCall& call) + { + ServerIndex& index = OrthancRestApi::GetIndex(call); + + std::list<std::string> a, b, c; + a.push_back(call.GetUriComponent("id", "")); + + ResourceType type = start; + while (type != end) + { + b.clear(); + + for (std::list<std::string>::const_iterator + it = a.begin(); it != a.end(); ++it) + { + index.GetChildren(c, *it); + b.splice(b.begin(), c); + } + + type = GetChildResourceType(type); + + a.clear(); + a.splice(a.begin(), b); + } + + Json::Value result = Json::arrayValue; + + for (std::list<std::string>::const_iterator + it = a.begin(); it != a.end(); ++it) + { + Json::Value item; + + if (OrthancRestApi::GetIndex(call).LookupResource(item, *it, end)) + { + result.append(item); + } + } + + call.GetOutput().AnswerJson(result); + } + + + static void GetChildInstancesTags(RestApiGetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + std::string publicId = call.GetUriComponent("id", ""); + DicomToJsonFormat format = GetDicomFormat(call); + + std::set<DicomTag> ignoreTagLength; + ParseSetOfTags(ignoreTagLength, call, "ignore-length"); + + // Retrieve all the instances of this patient/study/series + typedef std::list<std::string> Instances; + Instances instances; + + context.GetIndex().GetChildInstances(instances, publicId); // (*) + + Json::Value result = Json::objectValue; + + for (Instances::const_iterator it = instances.begin(); + it != instances.end(); ++it) + { + Json::Value full; + context.ReadDicomAsJson(full, *it, ignoreTagLength); + + if (format != DicomToJsonFormat_Full) + { + Json::Value simplified; + ServerToolbox::SimplifyTags(simplified, full, format); + result[*it] = simplified; + } + else + { + result[*it] = full; + } + } + + call.GetOutput().AnswerJson(result); + } + + + + template <enum ResourceType start, + enum ResourceType end> + static void GetParentResource(RestApiGetCall& call) + { + assert(start > end); + + ServerIndex& index = OrthancRestApi::GetIndex(call); + + std::string current = call.GetUriComponent("id", ""); + ResourceType currentType = start; + while (currentType > end) + { + std::string parent; + if (!index.LookupParent(parent, current)) + { + // Error that could happen if the resource gets deleted by + // another concurrent call + return; + } + + current = parent; + currentType = GetParentResourceType(currentType); + } + + assert(currentType == end); + + Json::Value result; + if (index.LookupResource(result, current, end)) + { + call.GetOutput().AnswerJson(result); + } + } + + + static void ExtractPdf(RestApiGetCall& call) + { + const std::string id = call.GetUriComponent("id", ""); + + std::string pdf; + ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), id); + + if (locker.GetDicom().ExtractPdf(pdf)) + { + call.GetOutput().AnswerBuffer(pdf, MimeType_Pdf); + return; + } + } + + + static void OrderSlices(RestApiGetCall& call) + { + const std::string id = call.GetUriComponent("id", ""); + + ServerIndex& index = OrthancRestApi::GetIndex(call); + SliceOrdering ordering(index, id); + + Json::Value result; + ordering.Format(result); + call.GetOutput().AnswerJson(result); + } + + + static void GetInstanceHeader(RestApiGetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string publicId = call.GetUriComponent("id", ""); + + std::string dicomContent; + context.ReadDicom(dicomContent, publicId); + + // TODO Consider using "DicomMap::ParseDicomMetaInformation()" to + // speed up things here + + ParsedDicomFile dicom(dicomContent); + + Json::Value header; + dicom.HeaderToJson(header, DicomToJsonFormat_Full); + + AnswerDicomAsJson(call, header); + } + + + static void InvalidateTags(RestApiPostCall& call) + { + ServerIndex& index = OrthancRestApi::GetIndex(call); + + // Loop over the instances, grouping them by parent studies so as + // to avoid large memory consumption + std::list<std::string> studies; + index.GetAllUuids(studies, ResourceType_Study); + + for (std::list<std::string>::const_iterator + study = studies.begin(); study != studies.end(); ++study) + { + std::list<std::string> instances; + index.GetChildInstances(instances, *study); + + for (std::list<std::string>::const_iterator + instance = instances.begin(); instance != instances.end(); ++instance) + { + index.DeleteAttachment(*instance, FileContentType_DicomAsJson); + } + } + + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + + + template <enum ResourceType type> + static void ReconstructResource(RestApiPostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + ServerToolbox::ReconstructResource(context, call.GetUriComponent("id", "")); + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + + + static void ReconstructAllResources(RestApiPostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::list<std::string> studies; + context.GetIndex().GetAllUuids(studies, ResourceType_Study); + + for (std::list<std::string>::const_iterator + study = studies.begin(); study != studies.end(); ++study) + { + ServerToolbox::ReconstructResource(context, *study); + } + + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + + + void OrthancRestApi::RegisterResources() + { + Register("/instances", ListResources<ResourceType_Instance>); + Register("/patients", ListResources<ResourceType_Patient>); + Register("/series", ListResources<ResourceType_Series>); + Register("/studies", ListResources<ResourceType_Study>); + + Register("/instances/{id}", DeleteSingleResource<ResourceType_Instance>); + Register("/instances/{id}", GetSingleResource<ResourceType_Instance>); + Register("/patients/{id}", DeleteSingleResource<ResourceType_Patient>); + Register("/patients/{id}", GetSingleResource<ResourceType_Patient>); + Register("/series/{id}", DeleteSingleResource<ResourceType_Series>); + Register("/series/{id}", GetSingleResource<ResourceType_Series>); + Register("/studies/{id}", DeleteSingleResource<ResourceType_Study>); + Register("/studies/{id}", GetSingleResource<ResourceType_Study>); + + Register("/instances/{id}/statistics", GetResourceStatistics); + Register("/patients/{id}/statistics", GetResourceStatistics); + Register("/studies/{id}/statistics", GetResourceStatistics); + Register("/series/{id}/statistics", GetResourceStatistics); + + Register("/patients/{id}/shared-tags", GetSharedTags); + Register("/series/{id}/shared-tags", GetSharedTags); + Register("/studies/{id}/shared-tags", GetSharedTags); + + Register("/instances/{id}/module", GetModule<ResourceType_Instance, DicomModule_Instance>); + Register("/patients/{id}/module", GetModule<ResourceType_Patient, DicomModule_Patient>); + Register("/series/{id}/module", GetModule<ResourceType_Series, DicomModule_Series>); + Register("/studies/{id}/module", GetModule<ResourceType_Study, DicomModule_Study>); + Register("/studies/{id}/module-patient", GetModule<ResourceType_Study, DicomModule_Patient>); + + Register("/instances/{id}/file", GetInstanceFile); + Register("/instances/{id}/export", ExportInstanceFile); + Register("/instances/{id}/tags", GetInstanceTagsBis); + Register("/instances/{id}/simplified-tags", GetInstanceTags<DicomToJsonFormat_Human>); + Register("/instances/{id}/frames", ListFrames); + + Register("/instances/{id}/frames/{frame}/preview", GetImage<ImageExtractionMode_Preview>); + Register("/instances/{id}/frames/{frame}/rendered", GetRenderedFrame); + Register("/instances/{id}/frames/{frame}/image-uint8", GetImage<ImageExtractionMode_UInt8>); + Register("/instances/{id}/frames/{frame}/image-uint16", GetImage<ImageExtractionMode_UInt16>); + Register("/instances/{id}/frames/{frame}/image-int16", GetImage<ImageExtractionMode_Int16>); + Register("/instances/{id}/frames/{frame}/matlab", GetMatlabImage); + Register("/instances/{id}/frames/{frame}/raw", GetRawFrame<false>); + Register("/instances/{id}/frames/{frame}/raw.gz", GetRawFrame<true>); + Register("/instances/{id}/pdf", ExtractPdf); + Register("/instances/{id}/preview", GetImage<ImageExtractionMode_Preview>); + Register("/instances/{id}/rendered", GetRenderedFrame); + Register("/instances/{id}/image-uint8", GetImage<ImageExtractionMode_UInt8>); + Register("/instances/{id}/image-uint16", GetImage<ImageExtractionMode_UInt16>); + Register("/instances/{id}/image-int16", GetImage<ImageExtractionMode_Int16>); + Register("/instances/{id}/matlab", GetMatlabImage); + Register("/instances/{id}/header", GetInstanceHeader); + + Register("/patients/{id}/protected", IsProtectedPatient); + Register("/patients/{id}/protected", SetPatientProtection); + + Register("/{resourceType}/{id}/metadata", ListMetadata); + Register("/{resourceType}/{id}/metadata/{name}", DeleteMetadata); + Register("/{resourceType}/{id}/metadata/{name}", GetMetadata); + Register("/{resourceType}/{id}/metadata/{name}", SetMetadata); + + Register("/{resourceType}/{id}/attachments", ListAttachments); + Register("/{resourceType}/{id}/attachments/{name}", DeleteAttachment); + Register("/{resourceType}/{id}/attachments/{name}", GetAttachmentOperations); + Register("/{resourceType}/{id}/attachments/{name}", UploadAttachment); + Register("/{resourceType}/{id}/attachments/{name}/compress", ChangeAttachmentCompression<CompressionType_ZlibWithSize>); + Register("/{resourceType}/{id}/attachments/{name}/compressed-data", GetAttachmentData<0>); + Register("/{resourceType}/{id}/attachments/{name}/compressed-md5", GetAttachmentCompressedMD5); + Register("/{resourceType}/{id}/attachments/{name}/compressed-size", GetAttachmentCompressedSize); + Register("/{resourceType}/{id}/attachments/{name}/data", GetAttachmentData<1>); + Register("/{resourceType}/{id}/attachments/{name}/is-compressed", IsAttachmentCompressed); + Register("/{resourceType}/{id}/attachments/{name}/md5", GetAttachmentMD5); + Register("/{resourceType}/{id}/attachments/{name}/size", GetAttachmentSize); + Register("/{resourceType}/{id}/attachments/{name}/uncompress", ChangeAttachmentCompression<CompressionType_None>); + Register("/{resourceType}/{id}/attachments/{name}/verify-md5", VerifyAttachment); + + Register("/tools/invalidate-tags", InvalidateTags); + Register("/tools/lookup", Lookup); + Register("/tools/find", Find); + + Register("/patients/{id}/studies", GetChildResources<ResourceType_Patient, ResourceType_Study>); + Register("/patients/{id}/series", GetChildResources<ResourceType_Patient, ResourceType_Series>); + Register("/patients/{id}/instances", GetChildResources<ResourceType_Patient, ResourceType_Instance>); + Register("/studies/{id}/series", GetChildResources<ResourceType_Study, ResourceType_Series>); + Register("/studies/{id}/instances", GetChildResources<ResourceType_Study, ResourceType_Instance>); + Register("/series/{id}/instances", GetChildResources<ResourceType_Series, ResourceType_Instance>); + + Register("/studies/{id}/patient", GetParentResource<ResourceType_Study, ResourceType_Patient>); + Register("/series/{id}/patient", GetParentResource<ResourceType_Series, ResourceType_Patient>); + Register("/series/{id}/study", GetParentResource<ResourceType_Series, ResourceType_Study>); + Register("/instances/{id}/patient", GetParentResource<ResourceType_Instance, ResourceType_Patient>); + Register("/instances/{id}/study", GetParentResource<ResourceType_Instance, ResourceType_Study>); + Register("/instances/{id}/series", GetParentResource<ResourceType_Instance, ResourceType_Series>); + + Register("/patients/{id}/instances-tags", GetChildInstancesTags); + Register("/studies/{id}/instances-tags", GetChildInstancesTags); + Register("/series/{id}/instances-tags", GetChildInstancesTags); + + Register("/instances/{id}/content/*", GetRawContent); + + Register("/series/{id}/ordered-slices", OrderSlices); + + Register("/patients/{id}/reconstruct", ReconstructResource<ResourceType_Patient>); + Register("/studies/{id}/reconstruct", ReconstructResource<ResourceType_Study>); + Register("/series/{id}/reconstruct", ReconstructResource<ResourceType_Series>); + Register("/instances/{id}/reconstruct", ReconstructResource<ResourceType_Instance>); + Register("/tools/reconstruct", ReconstructAllResources); + } +}