# HG changeset patch # User Sebastien Jodogne # Date 1589987004 -7200 # Node ID 596912ebab5fce27542b7b582432ebb4a511dbb8 # Parent 66879215cbf3859ada0c6bfc48a6afc7e3c8df60# Parent 6e14f2da7c7e338f67c2ec5a27cad7cf7738c2fc integration mainline->c-get diff -r 66879215cbf3 -r 596912ebab5f CMakeLists.txt --- a/CMakeLists.txt Wed May 20 16:38:33 2020 +0200 +++ b/CMakeLists.txt Wed May 20 17:03:24 2020 +0200 @@ -91,6 +91,7 @@ OrthancServer/ServerEnumerations.cpp OrthancServer/ServerIndex.cpp OrthancServer/ServerJobs/ArchiveJob.cpp + OrthancServer/ServerJobs/CleaningInstancesJob.cpp OrthancServer/ServerJobs/DicomModalityStoreJob.cpp OrthancServer/ServerJobs/DicomMoveScuJob.cpp OrthancServer/ServerJobs/LuaJobManager.cpp diff -r 66879215cbf3 -r 596912ebab5f Core/Compression/HierarchicalZipWriter.h --- a/Core/Compression/HierarchicalZipWriter.h Wed May 20 16:38:33 2020 +0200 +++ b/Core/Compression/HierarchicalZipWriter.h Wed May 20 17:03:24 2020 +0200 @@ -140,7 +140,7 @@ return indexer_.GetCurrentDirectoryPath(); } - void Write(const char* data, size_t length) + void Write(const void* data, size_t length) { writer_.Write(data, length); } diff -r 66879215cbf3 -r 596912ebab5f Core/Compression/ZipWriter.cpp --- a/Core/Compression/ZipWriter.cpp Wed May 20 16:38:33 2020 +0200 +++ b/Core/Compression/ZipWriter.cpp Wed May 20 17:03:24 2020 +0200 @@ -227,7 +227,7 @@ } - void ZipWriter::Write(const char* data, size_t length) + void ZipWriter::Write(const void* data, size_t length) { if (!hasFileInZip_) { @@ -236,17 +236,19 @@ const size_t maxBytesInAStep = std::numeric_limits::max(); + const char* p = reinterpret_cast(data); + while (length > 0) { int bytes = static_cast(length <= maxBytesInAStep ? length : maxBytesInAStep); - if (zipWriteInFileInZip(pimpl_->file_, data, bytes)) + if (zipWriteInFileInZip(pimpl_->file_, p, bytes)) { throw OrthancException(ErrorCode_CannotWriteFile, "Cannot write data to ZIP archive: " + path_); } - data += bytes; + p += bytes; length -= bytes; } } diff -r 66879215cbf3 -r 596912ebab5f Core/Compression/ZipWriter.h --- a/Core/Compression/ZipWriter.h Wed May 20 16:38:33 2020 +0200 +++ b/Core/Compression/ZipWriter.h Wed May 20 17:03:24 2020 +0200 @@ -101,7 +101,7 @@ void OpenFile(const char* path); - void Write(const char* data, size_t length); + void Write(const void* data, size_t length); void Write(const std::string& data); }; diff -r 66879215cbf3 -r 596912ebab5f Core/DicomNetworking/DicomStoreUserConnection.cpp --- a/Core/DicomNetworking/DicomStoreUserConnection.cpp Wed May 20 16:38:33 2020 +0200 +++ b/Core/DicomNetworking/DicomStoreUserConnection.cpp Wed May 20 17:03:24 2020 +0200 @@ -491,40 +491,35 @@ uncompressedSyntaxes.insert(DicomTransferSyntax_BigEndianExplicit); } - std::unique_ptr transcoded( - transcoder.TranscodeToParsed(*dicom, buffer, size, uncompressedSyntaxes, false)); + IDicomTranscoder::DicomImage source; + source.AcquireParsed(dicom.release()); + source.SetExternalBuffer(buffer, size); - // WARNING: Below this point, "transcoded->GetDicom()" is possibly - // a reference to "*dicom", if the DCMTK transcoder was used + const std::string sourceUid = IDicomTranscoder::GetSopInstanceUid(source.GetParsed()); - if (transcoded.get() == NULL || - transcoded->GetDicom().getDataset() == NULL) + IDicomTranscoder::DicomImage transcoded; + if (transcoder.Transcode(transcoded, source, uncompressedSyntaxes, false)) { - throw OrthancException( - ErrorCode_NotImplemented, - "Cannot transcode from \"" + std::string(GetTransferSyntaxUid(inputSyntax)) + - "\" to an uncompressed syntax for modality: " + - GetParameters().GetRemoteModality().GetApplicationEntityTitle()); - } - else if (transcoded->HasSopInstanceUidChanged()) - { - throw OrthancException(ErrorCode_Plugin, "The transcoder has changed the SOP " - "instance UID while transcoding to an uncompressed transfer syntax"); - } - else - { - DicomTransferSyntax transcodedSyntax; - - // Sanity check - if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transcodedSyntax, transcoded->GetDicom()) || - accepted.find(transcodedSyntax) == accepted.end()) + if (sourceUid != IDicomTranscoder::GetSopInstanceUid(transcoded.GetParsed())) { - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_Plugin, "The transcoder has changed the SOP " + "instance UID while transcoding to an uncompressed transfer syntax"); } else { - Store(sopClassUid, sopInstanceUid, transcoded->GetDicom(), - hasMoveOriginator, moveOriginatorAET, moveOriginatorID); + DicomTransferSyntax transcodedSyntax; + + // Sanity check + if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transcodedSyntax, transcoded.GetParsed()) || + accepted.find(transcodedSyntax) == accepted.end()) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + Store(sopClassUid, sopInstanceUid, transcoded.GetParsed(), + hasMoveOriginator, moveOriginatorAET, moveOriginatorID); + } } } } diff -r 66879215cbf3 -r 596912ebab5f Core/DicomParsing/DcmtkTranscoder.cpp --- a/Core/DicomParsing/DcmtkTranscoder.cpp Wed May 20 16:38:33 2020 +0200 +++ b/Core/DicomParsing/DcmtkTranscoder.cpp Wed May 20 17:03:24 2020 +0200 @@ -55,81 +55,32 @@ namespace Orthanc { - static uint16_t GetBitsStored(DcmDataset& dataset) + static bool GetBitsStored(uint16_t& bitsStored, + DcmDataset& dataset) { - uint16_t bitsStored; - if (dataset.findAndGetUint16(DCM_BitsStored, bitsStored).good()) - { - return bitsStored; - } - else - { - throw OrthancException(ErrorCode_BadFileFormat, - "Missing \"Bits Stored\" tag in DICOM instance"); - } + return dataset.findAndGetUint16(DCM_BitsStored, bitsStored).good(); } - static std::string GetSopInstanceUid(DcmDataset& dataset) - { - const char* v = NULL; - - if (dataset.findAndGetString(DCM_SOPInstanceUID, v).good() && - v != NULL) - { - return std::string(v); - } - else - { - throw OrthancException(ErrorCode_BadFileFormat, "File without SOP instance UID"); - } - } - - - static void CheckSopInstanceUid(DcmFileFormat& dicom, - const std::string& sopInstanceUid, - bool mustEqual) - { - if (dicom.getDataset() == NULL) - { - throw OrthancException(ErrorCode_InternalError); - } - - bool ok; - - if (mustEqual) - { - ok = (GetSopInstanceUid(*dicom.getDataset()) == sopInstanceUid); - } - else - { - ok = (GetSopInstanceUid(*dicom.getDataset()) != sopInstanceUid); - } - - if (!ok) - { - throw OrthancException(ErrorCode_InternalError, - mustEqual ? "The SOP instance UID has changed unexpectedly during transcoding" : - "The SOP instance UID has not changed as expected during transcoding"); - } - } - - void DcmtkTranscoder::SetLossyQuality(unsigned int quality) { if (quality <= 0 || quality > 100) { - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException( + ErrorCode_ParameterOutOfRange, + "The quality for lossy transcoding must be an integer between 1 and 100, received: " + + boost::lexical_cast(quality)); } else { + LOG(INFO) << "Quality for lossy transcoding using DCMTK is set to: " << quality; lossyQuality_ = quality; } } - bool DcmtkTranscoder::InplaceTranscode(bool& hasSopInstanceUidChanged /* out */, + bool DcmtkTranscoder::InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */, DcmFileFormat& dicom, const std::set& allowedSyntaxes, bool allowNewSopInstanceUid) @@ -139,8 +90,6 @@ throw OrthancException(ErrorCode_InternalError); } - hasSopInstanceUidChanged = false; - DicomTransferSyntax syntax; if (!FromDcmtkBridge::LookupOrthancTransferSyntax(syntax, dicom)) { @@ -148,8 +97,10 @@ "Cannot determine the transfer syntax"); } - const uint16_t bitsStored = GetBitsStored(*dicom.getDataset()); - std::string sourceSopInstanceUid = GetSopInstanceUid(*dicom.getDataset()); + uint16_t bitsStored; + bool hasBitsStored = GetBitsStored(bitsStored, *dicom.getDataset()); + + std::string sourceSopInstanceUid = IDicomTranscoder::GetSopInstanceUid(dicom); if (allowedSyntaxes.find(syntax) != allowedSyntaxes.end()) { @@ -160,43 +111,42 @@ if (allowedSyntaxes.find(DicomTransferSyntax_LittleEndianImplicit) != allowedSyntaxes.end() && FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_LittleEndianImplicit, NULL)) { - CheckSopInstanceUid(dicom, sourceSopInstanceUid, true); + selectedSyntax = DicomTransferSyntax_LittleEndianImplicit; return true; } if (allowedSyntaxes.find(DicomTransferSyntax_LittleEndianExplicit) != allowedSyntaxes.end() && FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_LittleEndianExplicit, NULL)) { - CheckSopInstanceUid(dicom, sourceSopInstanceUid, true); + selectedSyntax = DicomTransferSyntax_LittleEndianExplicit; return true; } if (allowedSyntaxes.find(DicomTransferSyntax_BigEndianExplicit) != allowedSyntaxes.end() && FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_BigEndianExplicit, NULL)) { - CheckSopInstanceUid(dicom, sourceSopInstanceUid, true); + selectedSyntax = DicomTransferSyntax_BigEndianExplicit; return true; } if (allowedSyntaxes.find(DicomTransferSyntax_DeflatedLittleEndianExplicit) != allowedSyntaxes.end() && FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_DeflatedLittleEndianExplicit, NULL)) { - CheckSopInstanceUid(dicom, sourceSopInstanceUid, true); + selectedSyntax = DicomTransferSyntax_DeflatedLittleEndianExplicit; return true; } #if ORTHANC_ENABLE_DCMTK_JPEG == 1 if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess1) != allowedSyntaxes.end() && allowNewSopInstanceUid && - bitsStored == 8) + (!hasBitsStored || bitsStored == 8)) { // Check out "dcmjpeg/apps/dcmcjpeg.cc" DJ_RPLossy parameters(lossyQuality_); if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess1, ¶meters)) { - CheckSopInstanceUid(dicom, sourceSopInstanceUid, false); - hasSopInstanceUidChanged = true; + selectedSyntax = DicomTransferSyntax_JPEGProcess1; return true; } } @@ -205,14 +155,13 @@ #if ORTHANC_ENABLE_DCMTK_JPEG == 1 if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess2_4) != allowedSyntaxes.end() && allowNewSopInstanceUid && - bitsStored <= 12) + (!hasBitsStored || bitsStored <= 12)) { // Check out "dcmjpeg/apps/dcmcjpeg.cc" DJ_RPLossy parameters(lossyQuality_); if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess2_4, ¶meters)) { - CheckSopInstanceUid(dicom, sourceSopInstanceUid, false); - hasSopInstanceUidChanged = true; + selectedSyntax = DicomTransferSyntax_JPEGProcess2_4; return true; } } @@ -226,7 +175,7 @@ 0 /* opt_point_transform */); if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess14, ¶meters)) { - CheckSopInstanceUid(dicom, sourceSopInstanceUid, true); + selectedSyntax = DicomTransferSyntax_JPEGProcess14; return true; } } @@ -240,7 +189,7 @@ 0 /* opt_point_transform */); if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess14SV1, ¶meters)) { - CheckSopInstanceUid(dicom, sourceSopInstanceUid, true); + selectedSyntax = DicomTransferSyntax_JPEGProcess14SV1; return true; } } @@ -259,7 +208,7 @@ **/ if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGLSLossless, ¶meters)) { - CheckSopInstanceUid(dicom, sourceSopInstanceUid, true); + selectedSyntax = DicomTransferSyntax_JPEGLSLossless; return true; } } @@ -279,8 +228,7 @@ **/ if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGLSLossy, ¶meters)) { - CheckSopInstanceUid(dicom, sourceSopInstanceUid, false); - hasSopInstanceUidChanged = true; + selectedSyntax = DicomTransferSyntax_JPEGLSLossy; return true; } } @@ -322,81 +270,61 @@ } - - bool DcmtkTranscoder::TranscodeParsedToBuffer( - std::string& target /* out */, - DicomTransferSyntax& sourceSyntax /* out */, - bool& hasSopInstanceUidChanged /* out */, - DcmFileFormat& dicom /* in, possibly modified */, - DicomTransferSyntax targetSyntax, - bool allowNewSopInstanceUid) + bool DcmtkTranscoder::Transcode(DicomImage& target, + DicomImage& source /* in, "GetParsed()" possibly modified */, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) { - if (dicom.getDataset() == NULL) - { - throw OrthancException(ErrorCode_InternalError); - } - - if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, dicom)) + target.Clear(); + + DicomTransferSyntax sourceSyntax; + if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, source.GetParsed())) { LOG(ERROR) << "Unsupport transfer syntax for transcoding"; return false; } - std::set tmp; - tmp.insert(targetSyntax); +#if !defined(NDEBUG) + const std::string sourceSopInstanceUid = GetSopInstanceUid(source.GetParsed()); +#endif - if (InplaceTranscode(hasSopInstanceUidChanged, dicom, tmp, allowNewSopInstanceUid)) + DicomTransferSyntax targetSyntax; + if (allowedSyntaxes.find(sourceSyntax) != allowedSyntaxes.end()) { + // No transcoding is needed + target.AcquireParsed(source); + target.AcquireBuffer(source); + return true; + } + else if (InplaceTranscode(targetSyntax, source.GetParsed(), + allowedSyntaxes, allowNewSopInstanceUid)) + { + // Sanity check DicomTransferSyntax targetSyntax2; - if (FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax2, dicom) && + if (FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax2, source.GetParsed()) && targetSyntax == targetSyntax2 && - dicom.getDataset() != NULL) + allowedSyntaxes.find(targetSyntax2) != allowedSyntaxes.end()) { - FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom.getDataset()); + target.AcquireParsed(source); + source.Clear(); + +#if !defined(NDEBUG) + // Only run the sanity check in debug mode + CheckTranscoding(target, sourceSyntax, sourceSopInstanceUid, + allowedSyntaxes, allowNewSopInstanceUid); +#endif + return true; } else { throw OrthancException(ErrorCode_InternalError); - } - } - else - { - return false; - } - } - - - IDicomTranscoder::TranscodedDicom* DcmtkTranscoder::TranscodeToParsed( - DcmFileFormat& dicom /* in, possibly modified */, - const void* buffer /* in, same DICOM file as "dicom" */, - size_t size, - const std::set& allowedSyntaxes, - bool allowNewSopInstanceUid) - { - DicomTransferSyntax sourceSyntax; - if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, dicom)) - { - LOG(ERROR) << "Unsupport transfer syntax for transcoding"; - return NULL; - } - - bool hasSopInstanceUidChanged; - - if (allowedSyntaxes.find(sourceSyntax) != allowedSyntaxes.end()) - { - // No transcoding is needed - return TranscodedDicom::CreateFromExternal(dicom, false /* no change in UID */); - } - else if (InplaceTranscode(hasSopInstanceUidChanged, dicom, - allowedSyntaxes, allowNewSopInstanceUid)) - { - return TranscodedDicom::CreateFromExternal(dicom, hasSopInstanceUidChanged); + } } else { // Cannot transcode - return NULL; + return false; } } } diff -r 66879215cbf3 -r 596912ebab5f Core/DicomParsing/DcmtkTranscoder.h --- a/Core/DicomParsing/DcmtkTranscoder.h Wed May 20 16:38:33 2020 +0200 +++ b/Core/DicomParsing/DcmtkTranscoder.h Wed May 20 17:03:24 2020 +0200 @@ -50,6 +50,11 @@ private: unsigned int lossyQuality_; + bool InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */, + DcmFileFormat& dicom, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid); + public: DcmtkTranscoder() : lossyQuality_(90) @@ -63,25 +68,11 @@ return lossyQuality_; } - bool InplaceTranscode(bool& hasSopInstanceUidChanged /* out */, - DcmFileFormat& dicom, - const std::set& allowedSyntaxes, - bool allowNewSopInstanceUid); - static bool IsSupported(DicomTransferSyntax syntax); - virtual bool TranscodeParsedToBuffer(std::string& target /* out */, - DicomTransferSyntax& sourceSyntax /* out */, - bool& hasSopInstanceUidChanged /* out */, - DcmFileFormat& dicom /* in, possibly modified */, - DicomTransferSyntax targetSyntax, - bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; - - virtual TranscodedDicom* TranscodeToParsed( - DcmFileFormat& dicom /* in, possibly modified */, - const void* buffer /* in, same DICOM file as "dicom" */, - size_t size, - const std::set& allowedSyntaxes, - bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; + virtual bool Transcode(DicomImage& target, + DicomImage& source /* in, "GetParsed()" possibly modified */, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; }; } diff -r 66879215cbf3 -r 596912ebab5f Core/DicomParsing/FromDcmtkBridge.cpp --- a/Core/DicomParsing/FromDcmtkBridge.cpp Wed May 20 16:38:33 2020 +0200 +++ b/Core/DicomParsing/FromDcmtkBridge.cpp Wed May 20 17:03:24 2020 +0200 @@ -1299,9 +1299,9 @@ { DicomTransferSyntax sourceSyntax; bool known = LookupOrthancTransferSyntax(sourceSyntax, dicom); - - if (!dicom.getDataset()->chooseRepresentation(xfer, representation).good() || - !dicom.getDataset()->canWriteXfer(xfer) || + + if (!dicom.chooseRepresentation(xfer, representation).good() || + !dicom.canWriteXfer(xfer) || !dicom.validateMetaInfo(xfer, EWM_updateMeta).good()) { return false; @@ -1780,9 +1780,13 @@ } DcmPixelData& pixelData = dynamic_cast(*element); + + E_TransferSyntax repType; + const DcmRepresentationParameter *repParam = NULL; + pixelData.getCurrentRepresentationKey(repType, repParam); + DcmPixelSequence* pixelSequence = NULL; - if (!pixelData.getEncapsulatedRepresentation - (dataset.getCurrentXfer(), NULL, pixelSequence).good()) + if (!pixelData.getEncapsulatedRepresentation(repType, repParam, pixelSequence).good()) { return NULL; } diff -r 66879215cbf3 -r 596912ebab5f Core/DicomParsing/IDicomTranscoder.cpp --- a/Core/DicomParsing/IDicomTranscoder.cpp Wed May 20 16:38:33 2020 +0200 +++ b/Core/DicomParsing/IDicomTranscoder.cpp Wed May 20 17:03:24 2020 +0200 @@ -35,77 +35,404 @@ #include "IDicomTranscoder.h" #include "../OrthancException.h" +#include "FromDcmtkBridge.h" +#include "ParsedDicomFile.h" #include +#include namespace Orthanc { - IDicomTranscoder::TranscodedDicom::TranscodedDicom(bool hasSopInstanceUidChanged) : - external_(NULL), - hasSopInstanceUidChanged_(hasSopInstanceUidChanged) + IDicomTranscoder::TranscodingType IDicomTranscoder::GetTranscodingType(DicomTransferSyntax target, + DicomTransferSyntax source) { + if (target == source) + { + return TranscodingType_Lossless; + } + else if (target == DicomTransferSyntax_LittleEndianImplicit || + target == DicomTransferSyntax_LittleEndianExplicit || + target == DicomTransferSyntax_BigEndianExplicit || + target == DicomTransferSyntax_DeflatedLittleEndianExplicit || + target == DicomTransferSyntax_JPEGProcess14 || + target == DicomTransferSyntax_JPEGProcess14SV1 || + target == DicomTransferSyntax_JPEGLSLossless || + target == DicomTransferSyntax_JPEG2000LosslessOnly || + target == DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly) + { + return TranscodingType_Lossless; + } + else if (target == DicomTransferSyntax_JPEGProcess1 || + target == DicomTransferSyntax_JPEGProcess2_4 || + target == DicomTransferSyntax_JPEGLSLossy || + target == DicomTransferSyntax_JPEG2000 || + target == DicomTransferSyntax_JPEG2000Multicomponent) + { + return TranscodingType_Lossy; + } + else + { + return TranscodingType_Unknown; + } } - + - IDicomTranscoder::TranscodedDicom* - IDicomTranscoder::TranscodedDicom::CreateFromExternal(DcmFileFormat& dicom, - bool hasSopInstanceUidChanged) + std::string IDicomTranscoder::GetSopInstanceUid(DcmFileFormat& dicom) { - std::unique_ptr transcoded(new TranscodedDicom(hasSopInstanceUidChanged)); - transcoded->external_ = &dicom; - return transcoded.release(); - } + if (dicom.getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + DcmDataset& dataset = *dicom.getDataset(); + + const char* v = NULL; + + if (dataset.findAndGetString(DCM_SOPInstanceUID, v).good() && + v != NULL) + { + return std::string(v); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, "File without SOP instance UID"); + } + } + - - IDicomTranscoder::TranscodedDicom* - IDicomTranscoder::TranscodedDicom::CreateFromInternal(DcmFileFormat* dicom, - bool hasSopInstanceUidChanged) + void IDicomTranscoder::CheckTranscoding(IDicomTranscoder::DicomImage& transcoded, + DicomTransferSyntax sourceSyntax, + const std::string& sourceSopInstanceUid, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) { - if (dicom == NULL) + DcmFileFormat& parsed = transcoded.GetParsed(); + + if (parsed.getDataset() == NULL) { - throw OrthancException(ErrorCode_NullPointer); + throw OrthancException(ErrorCode_InternalError); + } + + std::string targetSopInstanceUid = GetSopInstanceUid(parsed); + + if (parsed.getDataset()->tagExists(DCM_PixelData)) + { + if (!allowNewSopInstanceUid && (targetSopInstanceUid != sourceSopInstanceUid)) + { + throw OrthancException(ErrorCode_InternalError); + } } else { - std::unique_ptr transcoded(new TranscodedDicom(hasSopInstanceUidChanged)); - transcoded->internal_.reset(dicom); - return transcoded.release(); + if (targetSopInstanceUid != sourceSopInstanceUid) + { + throw OrthancException(ErrorCode_InternalError, + "No pixel data: Transcoding must not change the SOP instance UID"); + } + } + + DicomTransferSyntax targetSyntax; + if (!FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax, parsed)) + { + return; // Unknown transfer syntax, cannot do further test + } + + if (allowedSyntaxes.find(sourceSyntax) != allowedSyntaxes.end()) + { + // No transcoding should have happened + if (targetSopInstanceUid != sourceSopInstanceUid) + { + throw OrthancException(ErrorCode_InternalError); + } + } + + if (allowedSyntaxes.find(targetSyntax) == allowedSyntaxes.end()) + { + throw OrthancException(ErrorCode_InternalError, "An incorrect output transfer syntax was chosen"); + } + + if (parsed.getDataset()->tagExists(DCM_PixelData)) + { + switch (GetTranscodingType(targetSyntax, sourceSyntax)) + { + case TranscodingType_Lossy: + if (targetSopInstanceUid == sourceSopInstanceUid) + { + throw OrthancException(ErrorCode_InternalError); + } + break; + + case TranscodingType_Lossless: + if (targetSopInstanceUid != sourceSopInstanceUid) + { + throw OrthancException(ErrorCode_InternalError); + } + break; + + default: + break; + } + } + } + + + void IDicomTranscoder::DicomImage::Parse() + { + if (parsed_.get() != NULL) + { + // Already parsed + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (buffer_.get() != NULL) + { + if (isExternalBuffer_) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + parsed_.reset(FromDcmtkBridge::LoadFromMemoryBuffer( + buffer_->empty() ? NULL : buffer_->c_str(), buffer_->size())); + + if (parsed_.get() == NULL) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + } + else if (isExternalBuffer_) + { + parsed_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(externalBuffer_, externalSize_)); + + if (parsed_.get() == NULL) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + else + { + // No buffer is available + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void IDicomTranscoder::DicomImage::Serialize() + { + if (parsed_.get() == NULL || + buffer_.get() != NULL || + isExternalBuffer_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (parsed_->getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + buffer_.reset(new std::string); + FromDcmtkBridge::SaveToMemoryBuffer(*buffer_, *parsed_->getDataset()); } } - DcmFileFormat& IDicomTranscoder::TranscodedDicom::GetDicom() const + IDicomTranscoder::DicomImage::DicomImage() : + isExternalBuffer_(false) + { + } + + + void IDicomTranscoder::DicomImage::Clear() { - if (internal_.get() != NULL) + parsed_.reset(NULL); + buffer_.reset(NULL); + isExternalBuffer_ = false; + } + + + void IDicomTranscoder::DicomImage::AcquireParsed(ParsedDicomFile& parsed) + { + AcquireParsed(parsed.ReleaseDcmtkObject()); + } + + + void IDicomTranscoder::DicomImage::AcquireParsed(DcmFileFormat* parsed) + { + if (parsed == NULL) { - return *internal_.get(); + throw OrthancException(ErrorCode_NullPointer); } - else if (external_ != NULL) + else if (parsed->getDataset() == NULL) { - return *external_; + throw OrthancException(ErrorCode_InternalError); + } + else if (parsed_.get() != NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); } else { - // Probably results from a call to "ReleaseDicom()" + parsed_.reset(parsed); + } + } + + + void IDicomTranscoder::DicomImage::AcquireParsed(DicomImage& other) + { + AcquireParsed(other.ReleaseParsed()); + } + + + void IDicomTranscoder::DicomImage::AcquireBuffer(std::string& buffer /* will be swapped */) + { + if (buffer_.get() != NULL || + isExternalBuffer_) + { throw OrthancException(ErrorCode_BadSequenceOfCalls); } + else + { + buffer_.reset(new std::string); + buffer_->swap(buffer); + } } - DcmFileFormat* IDicomTranscoder::TranscodedDicom::ReleaseDicom() + void IDicomTranscoder::DicomImage::AcquireBuffer(DicomImage& other) { - if (internal_.get() != NULL) + if (buffer_.get() != NULL || + isExternalBuffer_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (other.isExternalBuffer_) + { + assert(other.buffer_.get() == NULL); + isExternalBuffer_ = true; + externalBuffer_ = other.externalBuffer_; + externalSize_ = other.externalSize_; + } + else if (other.buffer_.get() != NULL) + { + buffer_.reset(other.buffer_.release()); + } + else { - return internal_.release(); + buffer_.reset(NULL); + } + } + + + void IDicomTranscoder::DicomImage::SetExternalBuffer(const void* buffer, + size_t size) + { + if (buffer_.get() != NULL || + isExternalBuffer_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + isExternalBuffer_ = true; + externalBuffer_ = buffer; + externalSize_ = size; } - else if (external_ != NULL) + } + + + void IDicomTranscoder::DicomImage::SetExternalBuffer(const std::string& buffer) + { + SetExternalBuffer(buffer.empty() ? NULL : buffer.c_str(), buffer.size()); + } + + + DcmFileFormat& IDicomTranscoder::DicomImage::GetParsed() + { + if (parsed_.get() != NULL) { - return new DcmFileFormat(*external_); // Clone + return *parsed_; + } + else if (buffer_.get() != NULL || + isExternalBuffer_) + { + Parse(); + return *parsed_; } else { - // Probably results from a call to "ReleaseDicom()" - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } + throw OrthancException( + ErrorCode_BadSequenceOfCalls, + "AcquireParsed(), AcquireBuffer() or SetExternalBuffer() should have been called"); + } + } + + + DcmFileFormat* IDicomTranscoder::DicomImage::ReleaseParsed() + { + if (parsed_.get() != NULL) + { + buffer_.reset(NULL); + return parsed_.release(); + } + else if (buffer_.get() != NULL || + isExternalBuffer_) + { + Parse(); + buffer_.reset(NULL); + return parsed_.release(); + } + else + { + throw OrthancException( + ErrorCode_BadSequenceOfCalls, + "AcquireParsed(), AcquireBuffer() or SetExternalBuffer() should have been called"); + } + } + + + ParsedDicomFile* IDicomTranscoder::DicomImage::ReleaseAsParsedDicomFile() + { + return ParsedDicomFile::AcquireDcmtkObject(ReleaseParsed()); + } + + + const void* IDicomTranscoder::DicomImage::GetBufferData() + { + if (isExternalBuffer_) + { + assert(buffer_.get() == NULL); + return externalBuffer_; + } + else + { + if (buffer_.get() == NULL) + { + Serialize(); + } + + assert(buffer_.get() != NULL); + return buffer_->empty() ? NULL : buffer_->c_str(); + } + } + + + size_t IDicomTranscoder::DicomImage::GetBufferSize() + { + if (isExternalBuffer_) + { + assert(buffer_.get() == NULL); + return externalSize_; + } + else + { + if (buffer_.get() == NULL) + { + Serialize(); + } + + assert(buffer_.get() != NULL); + return buffer_->size(); + } } } diff -r 66879215cbf3 -r 596912ebab5f Core/DicomParsing/IDicomTranscoder.h --- a/Core/DicomParsing/IDicomTranscoder.h Wed May 20 16:38:33 2020 +0200 +++ b/Core/DicomParsing/IDicomTranscoder.h Wed May 20 17:03:24 2020 +0200 @@ -47,60 +47,85 @@ * WARNING: This class might be called from several threads at * once. Make sure to implement proper locking. **/ + + class ParsedDicomFile; class IDicomTranscoder : public boost::noncopyable { public: + class DicomImage : public boost::noncopyable + { + private: + std::unique_ptr parsed_; + std::unique_ptr buffer_; + bool isExternalBuffer_; + const void* externalBuffer_; + size_t externalSize_; + + void Parse(); + + void Serialize(); + + DcmFileFormat* ReleaseParsed(); + + public: + DicomImage(); + + void Clear(); + + // Calling this method will invalidate the "ParsedDicomFile" object + void AcquireParsed(ParsedDicomFile& parsed); + + void AcquireParsed(DcmFileFormat* parsed); + + void AcquireParsed(DicomImage& other); + + void AcquireBuffer(std::string& buffer /* will be swapped */); + + void AcquireBuffer(DicomImage& other); + + void SetExternalBuffer(const void* buffer, + size_t size); + + void SetExternalBuffer(const std::string& buffer); + + DcmFileFormat& GetParsed(); + + ParsedDicomFile* ReleaseAsParsedDicomFile(); + + const void* GetBufferData(); + + size_t GetBufferSize(); + }; + + + protected: + enum TranscodingType + { + TranscodingType_Lossy, + TranscodingType_Lossless, + TranscodingType_Unknown + }; + + static TranscodingType GetTranscodingType(DicomTransferSyntax target, + DicomTransferSyntax source); + + static void CheckTranscoding(DicomImage& transcoded, + DicomTransferSyntax sourceSyntax, + const std::string& sourceSopInstanceUid, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid); + + public: virtual ~IDicomTranscoder() { } - virtual bool TranscodeParsedToBuffer(std::string& target /* out */, - DicomTransferSyntax& sourceSyntax /* out */, - bool& hasSopInstanceUidChanged /* out */, - DcmFileFormat& dicom /* in, possibly modified */, - DicomTransferSyntax targetSyntax, - bool allowNewSopInstanceUid) = 0; - - - class TranscodedDicom : public boost::noncopyable - { - private: - std::unique_ptr internal_; - DcmFileFormat* external_; - bool hasSopInstanceUidChanged_; - - TranscodedDicom(bool hasSopInstanceUidChanged); - - public: - static TranscodedDicom* CreateFromExternal(DcmFileFormat& dicom, - bool hasSopInstanceUidChanged); - - static TranscodedDicom* CreateFromInternal(DcmFileFormat* dicom, - bool hasSopInstanceUidChanged); + virtual bool Transcode(DicomImage& target, + DicomImage& source /* in, "GetParsed()" possibly modified */, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) = 0; - // TODO - Is this information used somewhere? - bool HasSopInstanceUidChanged() const - { - return hasSopInstanceUidChanged_; - } - - DcmFileFormat& GetDicom() const; - - DcmFileFormat* ReleaseDicom(); - }; - - /** - * Transcoding flavor that creates a new parsed DICOM file. A - * "std::set<>" is used to give the possible plugin the - * possibility to do a single parsing for all the possible - * transfer syntaxes. This flavor is used by C-STORE. - **/ - virtual TranscodedDicom* TranscodeToParsed( - DcmFileFormat& dicom /* in, possibly modified */, - const void* buffer /* in, same DICOM file as "dicom" */, - size_t size, - const std::set& allowedSyntaxes, - bool allowNewSopInstanceUid) = 0; + static std::string GetSopInstanceUid(DcmFileFormat& dicom); }; } diff -r 66879215cbf3 -r 596912ebab5f Core/DicomParsing/Internals/DicomImageDecoder.cpp --- a/Core/DicomParsing/Internals/DicomImageDecoder.cpp Wed May 20 16:38:33 2020 +0200 +++ b/Core/DicomParsing/Internals/DicomImageDecoder.cpp Wed May 20 17:03:24 2020 +0200 @@ -810,7 +810,7 @@ **/ { - LOG(INFO) << "Decoding a compressed image by converting its transfer syntax to Little Endian"; + LOG(INFO) << "Trying to decode a compressed image by transcoding it to Little Endian Explicit"; std::unique_ptr converted(dynamic_cast(dataset.clone())); converted->chooseRepresentation(EXS_LittleEndianExplicit, NULL); @@ -821,8 +821,18 @@ } } - throw OrthancException(ErrorCode_BadFileFormat, - "Cannot decode a DICOM image with the built-in decoder"); + DicomTransferSyntax s; + if (FromDcmtkBridge::LookupOrthancTransferSyntax(s, dataset.getCurrentXfer())) + { + throw OrthancException(ErrorCode_NotImplemented, + "The built-in DCMTK decoder cannot decode some DICOM instance " + "whose transfer syntax is: " + std::string(GetTransferSyntaxUid(s))); + } + else + { + throw OrthancException(ErrorCode_NotImplemented, + "The built-in DCMTK decoder cannot decode some DICOM instance"); + } } diff -r 66879215cbf3 -r 596912ebab5f Core/DicomParsing/MemoryBufferTranscoder.cpp --- a/Core/DicomParsing/MemoryBufferTranscoder.cpp Wed May 20 16:38:33 2020 +0200 +++ b/Core/DicomParsing/MemoryBufferTranscoder.cpp Wed May 20 17:03:24 2020 +0200 @@ -37,105 +37,73 @@ #include "../OrthancException.h" #include "FromDcmtkBridge.h" +#if !defined(NDEBUG) // For debugging +# include "ParsedDicomFile.h" +#endif + namespace Orthanc { - MemoryBufferTranscoder::MemoryBufferTranscoder() + static void CheckTargetSyntax(const std::string& transcoded, + const std::set& allowedSyntaxes) { -#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 - useDcmtk_ = true; -#else - useDcmtk_ = false; +#if !defined(NDEBUG) + // Debug mode + ParsedDicomFile parsed(transcoded); + + std::string s; + DicomTransferSyntax a, b; + if (!parsed.LookupTransferSyntax(s) || + !FromDcmtkBridge::LookupOrthancTransferSyntax(a, parsed.GetDcmtkObject()) || + !LookupTransferSyntax(b, s) || + a != b || + allowedSyntaxes.find(a) == allowedSyntaxes.end()) + { + throw OrthancException( + ErrorCode_Plugin, + "DEBUG - The transcoding plugin has not written to one of the allowed transfer syntaxes"); + } #endif } - - - void MemoryBufferTranscoder::SetDcmtkUsed(bool used) - { -#if ORTHANC_ENABLE_DCMTK_TRANSCODING != 1 - if (useDcmtk) - { - throw OrthancException(ErrorCode_NotImplemented, - "Orthanc was built without support for DMCTK transcoding"); - } -#endif - - useDcmtk_ = used; - } - + - bool MemoryBufferTranscoder::TranscodeParsedToBuffer( - std::string& target /* out */, - DicomTransferSyntax& sourceSyntax /* out */, - bool& hasSopInstanceUidChanged /* out */, - DcmFileFormat& dicom /* in, possibly modified */, - DicomTransferSyntax targetSyntax, - bool allowNewSopInstanceUid) + bool MemoryBufferTranscoder::Transcode(DicomImage& target, + DicomImage& source, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) { - if (dicom.getDataset() == NULL) + target.Clear(); + +#if !defined(NDEBUG) + // Don't run this code in release mode, as it implies parsing the DICOM file + DicomTransferSyntax sourceSyntax; + if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, source.GetParsed())) { - throw OrthancException(ErrorCode_InternalError); - } - - std::string source; - FromDcmtkBridge::SaveToMemoryBuffer(source, *dicom.getDataset()); - - const void* data = source.empty() ? NULL : source.c_str(); - - std::set tmp; - tmp.insert(targetSyntax); - - DicomTransferSyntax targetSyntax2; - bool success = Transcode(target, sourceSyntax, targetSyntax2, hasSopInstanceUidChanged, - data, source.size(), tmp, allowNewSopInstanceUid); - - if (success && - targetSyntax != targetSyntax2) - { - throw OrthancException(ErrorCode_InternalError); + LOG(ERROR) << "Unsupport transfer syntax for transcoding"; + return false; } -#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 - if (!success && - useDcmtk_ && - dcmtk_.TranscodeParsedToBuffer( - target, sourceSyntax, hasSopInstanceUidChanged, - dicom, targetSyntax, allowNewSopInstanceUid)) - { - success = true; - } + const std::string sourceSopInstanceUid = GetSopInstanceUid(source.GetParsed()); #endif - return success; - } - + std::string buffer; + if (TranscodeBuffer(buffer, source.GetBufferData(), source.GetBufferSize(), + allowedSyntaxes, allowNewSopInstanceUid)) + { + CheckTargetSyntax(buffer, allowedSyntaxes); // For debug only - IDicomTranscoder::TranscodedDicom* MemoryBufferTranscoder::TranscodeToParsed( - DcmFileFormat& dicom /* in, possibly modified */, - const void* buffer /* in, same DICOM file as "dicom" */, - size_t size, - const std::set& allowedSyntaxes, - bool allowNewSopInstanceUid) - { - DicomTransferSyntax sourceSyntax, targetSyntax; - bool hasSopInstanceUidChanged; - - std::string target; - if (Transcode(target, sourceSyntax, targetSyntax, hasSopInstanceUidChanged, - buffer, size, allowedSyntaxes, allowNewSopInstanceUid)) - { - const void* data = target.empty() ? NULL : target.c_str(); - return IDicomTranscoder::TranscodedDicom::CreateFromInternal( - FromDcmtkBridge::LoadFromMemoryBuffer(data, target.size()), hasSopInstanceUidChanged); + target.AcquireBuffer(buffer); + +#if !defined(NDEBUG) + // Only run the sanity check in debug mode + CheckTranscoding(target, sourceSyntax, sourceSopInstanceUid, + allowedSyntaxes, allowNewSopInstanceUid); +#endif + + return true; } -#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 - else if (useDcmtk_) - { - return dcmtk_.TranscodeToParsed(dicom, buffer, size, allowedSyntaxes, allowNewSopInstanceUid); - } -#endif else { - return NULL; + return false; } } } diff -r 66879215cbf3 -r 596912ebab5f Core/DicomParsing/MemoryBufferTranscoder.h --- a/Core/DicomParsing/MemoryBufferTranscoder.h Wed May 20 16:38:33 2020 +0200 +++ b/Core/DicomParsing/MemoryBufferTranscoder.h Wed May 20 17:03:24 2020 +0200 @@ -33,62 +33,24 @@ #pragma once -#if !defined(ORTHANC_ENABLE_DCMTK_TRANSCODING) -# error Macro ORTHANC_ENABLE_DCMTK_TRANSCODING must be defined to use this file -#endif - -#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 -# include "DcmtkTranscoder.h" -#endif +#include "IDicomTranscoder.h" namespace Orthanc { // This is the basis class for transcoding plugins class MemoryBufferTranscoder : public IDicomTranscoder { - private: - bool useDcmtk_; - -#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 - DcmtkTranscoder dcmtk_; -#endif - protected: - virtual bool Transcode(std::string& target, - DicomTransferSyntax& sourceSyntax /* out */, - DicomTransferSyntax& targetSyntax /* out */, - bool& hasSopInstanceUidChanged /* out */, - const void* buffer, - size_t size, - const std::set& allowedSyntaxes, - bool allowNewSopInstanceUid) = 0; + virtual bool TranscodeBuffer(std::string& target, + const void* buffer, + size_t size, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) = 0; public: - /** - * If "useDcmtk" is "true", the transcoder will first try and call - * DCMTK, before calling its own "Transcode()" implementation. - **/ - MemoryBufferTranscoder(); - - void SetDcmtkUsed(bool used); - - bool IsDcmtkUsed() const - { - return useDcmtk_; - } - - virtual bool TranscodeParsedToBuffer(std::string& target /* out */, - DicomTransferSyntax& sourceSyntax /* out */, - bool& hasSopInstanceUidChanged /* out */, - DcmFileFormat& dicom /* in, possibly modified */, - DicomTransferSyntax targetSyntax, - bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; - - virtual TranscodedDicom* TranscodeToParsed( - DcmFileFormat& dicom /* in, possibly modified */, - const void* buffer /* in, same DICOM file as "dicom" */, - size_t size, - const std::set& allowedSyntaxes, - bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; + virtual bool Transcode(DicomImage& target /* out */, + DicomImage& source, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; }; } diff -r 66879215cbf3 -r 596912ebab5f Core/DicomParsing/ParsedDicomFile.cpp --- a/Core/DicomParsing/ParsedDicomFile.cpp Wed May 20 16:38:33 2020 +0200 +++ b/Core/DicomParsing/ParsedDicomFile.cpp Wed May 20 17:03:24 2020 +0200 @@ -455,8 +455,8 @@ void ParsedDicomFile::SendPathValue(RestApiOutput& output, const UriComponents& uri) { - DcmItem* dicom = pimpl_->file_->getDataset(); - E_TransferSyntax transferSyntax = pimpl_->file_->getDataset()->getCurrentXfer(); + DcmItem* dicom = GetDcmtkObject().getDataset(); + E_TransferSyntax transferSyntax = GetDcmtkObject().getDataset()->getCurrentXfer(); // Special case: Accessing the pixel data if (uri.size() == 1 || @@ -516,7 +516,7 @@ InvalidateCache(); DcmTagKey key(tag.GetGroup(), tag.GetElement()); - DcmElement* element = pimpl_->file_->getDataset()->remove(key); + DcmElement* element = GetDcmtkObject().getDataset()->remove(key); if (element != NULL) { delete element; @@ -536,7 +536,7 @@ InvalidateCache(); - DcmItem* dicom = pimpl_->file_->getDataset(); + DcmItem* dicom = GetDcmtkObject().getDataset(); DcmTagKey key(tag.GetGroup(), tag.GetElement()); if (onlyIfExists && @@ -558,7 +558,7 @@ { InvalidateCache(); - DcmDataset& dataset = *pimpl_->file_->getDataset(); + DcmDataset& dataset = *GetDcmtkObject().getDataset(); // Loop over the dataset to detect its private tags typedef std::list Tags; @@ -629,7 +629,7 @@ return; } - if (pimpl_->file_->getDataset()->tagExists(ToDcmtkBridge::Convert(tag))) + if (GetDcmtkObject().getDataset()->tagExists(ToDcmtkBridge::Convert(tag))) { throw OrthancException(ErrorCode_AlreadyExistingTag); } @@ -650,7 +650,7 @@ bool hasCodeExtensions; Encoding encoding = DetectEncoding(hasCodeExtensions); std::unique_ptr element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator)); - InsertInternal(*pimpl_->file_->getDataset(), element.release()); + InsertInternal(*GetDcmtkObject().getDataset(), element.release()); } @@ -782,7 +782,7 @@ InvalidateCache(); - DcmDataset& dicom = *pimpl_->file_->getDataset(); + DcmDataset& dicom = *GetDcmtkObject().getDataset(); if (CanReplaceProceed(dicom, ToDcmtkBridge::Convert(tag), mode)) { // Either the tag was previously existing (and now removed), or @@ -828,7 +828,7 @@ InvalidateCache(); - DcmDataset& dicom = *pimpl_->file_->getDataset(); + DcmDataset& dicom = *GetDcmtkObject().getDataset(); if (CanReplaceProceed(dicom, ToDcmtkBridge::Convert(tag), mode)) { // Either the tag was previously existing (and now removed), or @@ -867,7 +867,7 @@ void ParsedDicomFile::Answer(RestApiOutput& output) { std::string serialized; - if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, *pimpl_->file_->getDataset())) + if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, *GetDcmtkObject().getDataset())) { output.AnswerBuffer(serialized, MimeType_Dicom); } @@ -879,7 +879,7 @@ const DicomTag& tag) { DcmTagKey k(tag.GetGroup(), tag.GetElement()); - DcmDataset& dataset = *pimpl_->file_->getDataset(); + DcmDataset& dataset = *GetDcmtkObject().getDataset(); if (tag.IsPrivate() || FromDcmtkBridge::IsUnknownTag(tag) || @@ -970,7 +970,7 @@ void ParsedDicomFile::SaveToMemoryBuffer(std::string& buffer) { - FromDcmtkBridge::SaveToMemoryBuffer(buffer, *pimpl_->file_->getDataset()); + FromDcmtkBridge::SaveToMemoryBuffer(buffer, *GetDcmtkObject().getDataset()); } @@ -1004,6 +1004,7 @@ bool permissive) { pimpl_->file_.reset(new DcmFileFormat); + pimpl_->frameIndex_.reset(NULL); const DicomValue* tmp = source.TestAndGetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET); @@ -1091,7 +1092,7 @@ bool keepSopInstanceUid) : pimpl_(new PImpl) { - pimpl_->file_.reset(dynamic_cast(other.pimpl_->file_->clone())); + pimpl_->file_.reset(dynamic_cast(other.GetDcmtkObject().clone())); if (!keepSopInstanceUid) { @@ -1121,7 +1122,30 @@ DcmFileFormat& ParsedDicomFile::GetDcmtkObject() const { - return *pimpl_->file_.get(); + if (pimpl_->file_.get() == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "ReleaseDcmtkObject() was called"); + } + else + { + return *pimpl_->file_; + } + } + + + DcmFileFormat* ParsedDicomFile::ReleaseDcmtkObject() + { + if (pimpl_->file_.get() == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "ReleaseDcmtkObject() was called"); + } + else + { + pimpl_->frameIndex_.reset(NULL); + return pimpl_->file_.release(); + } } @@ -1354,7 +1378,7 @@ } } - if (!pimpl_->file_->getDataset()->insert(pixels.release(), false, false).good()) + if (!GetDcmtkObject().getDataset()->insert(pixels.release(), false, false).good()) { throw OrthancException(ErrorCode_InternalError); } @@ -1364,7 +1388,7 @@ Encoding ParsedDicomFile::DetectEncoding(bool& hasCodeExtensions) const { return FromDcmtkBridge::DetectEncoding(hasCodeExtensions, - *pimpl_->file_->getDataset(), + *GetDcmtkObject().getDataset(), GetDefaultDicomEncoding()); } @@ -1388,7 +1412,7 @@ unsigned int maxStringLength) { std::set ignoreTagLength; - FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(), + FromDcmtkBridge::ExtractDicomAsJson(target, *GetDcmtkObject().getDataset(), format, flags, maxStringLength, GetDefaultDicomEncoding(), ignoreTagLength); } @@ -1400,7 +1424,7 @@ unsigned int maxStringLength, const std::set& ignoreTagLength) { - FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(), + FromDcmtkBridge::ExtractDicomAsJson(target, *GetDcmtkObject().getDataset(), format, flags, maxStringLength, GetDefaultDicomEncoding(), ignoreTagLength); } @@ -1409,28 +1433,28 @@ void ParsedDicomFile::DatasetToJson(Json::Value& target, const std::set& ignoreTagLength) { - FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(), ignoreTagLength); + FromDcmtkBridge::ExtractDicomAsJson(target, *GetDcmtkObject().getDataset(), ignoreTagLength); } void ParsedDicomFile::DatasetToJson(Json::Value& target) { const std::set ignoreTagLength; - FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(), ignoreTagLength); + FromDcmtkBridge::ExtractDicomAsJson(target, *GetDcmtkObject().getDataset(), ignoreTagLength); } void ParsedDicomFile::HeaderToJson(Json::Value& target, DicomToJsonFormat format) { - FromDcmtkBridge::ExtractHeaderAsJson(target, *pimpl_->file_->getMetaInfo(), format, DicomToJsonFlags_None, 0); + FromDcmtkBridge::ExtractHeaderAsJson(target, *GetDcmtkObject().getMetaInfo(), format, DicomToJsonFlags_None, 0); } bool ParsedDicomFile::HasTag(const DicomTag& tag) const { DcmTag key(tag.GetGroup(), tag.GetElement()); - return pimpl_->file_->getDataset()->tagExists(key); + return GetDcmtkObject().getDataset()->tagExists(key); } @@ -1472,7 +1496,7 @@ memcpy(bytes, pdf.c_str(), pdf.size()); DcmPolymorphOBOW* obj = element.release(); - result = pimpl_->file_->getDataset()->insert(obj); + result = GetDcmtkObject().getDataset()->insert(obj); if (!result.good()) { @@ -1564,13 +1588,13 @@ if (pimpl_->frameIndex_.get() == NULL) { assert(pimpl_->file_ != NULL && - pimpl_->file_->getDataset() != NULL); - pimpl_->frameIndex_.reset(new DicomFrameIndex(*pimpl_->file_->getDataset())); + GetDcmtkObject().getDataset() != NULL); + pimpl_->frameIndex_.reset(new DicomFrameIndex(*GetDcmtkObject().getDataset())); } pimpl_->frameIndex_->GetRawFrame(target, frameId); - E_TransferSyntax transferSyntax = pimpl_->file_->getDataset()->getCurrentXfer(); + E_TransferSyntax transferSyntax = GetDcmtkObject().getDataset()->getCurrentXfer(); switch (transferSyntax) { case EXS_JPEGProcess1: @@ -1598,8 +1622,8 @@ unsigned int ParsedDicomFile::GetFramesCount() const { assert(pimpl_->file_ != NULL && - pimpl_->file_->getDataset() != NULL); - return DicomFrameIndex::GetFramesCount(*pimpl_->file_->getDataset()); + GetDcmtkObject().getDataset() != NULL); + return DicomFrameIndex::GetFramesCount(*GetDcmtkObject().getDataset()); } @@ -1611,33 +1635,35 @@ if (source != target) // Avoid unnecessary conversion { ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, GetDicomSpecificCharacterSet(target)); - FromDcmtkBridge::ChangeStringEncoding(*pimpl_->file_->getDataset(), source, hasCodeExtensions, target); + FromDcmtkBridge::ChangeStringEncoding(*GetDcmtkObject().getDataset(), source, hasCodeExtensions, target); } } void ParsedDicomFile::ExtractDicomSummary(DicomMap& target) const { - FromDcmtkBridge::ExtractDicomSummary(target, *pimpl_->file_->getDataset()); + FromDcmtkBridge::ExtractDicomSummary(target, *GetDcmtkObject().getDataset()); } void ParsedDicomFile::ExtractDicomSummary(DicomMap& target, const std::set& ignoreTagLength) const { - FromDcmtkBridge::ExtractDicomSummary(target, *pimpl_->file_->getDataset(), ignoreTagLength); + FromDcmtkBridge::ExtractDicomSummary(target, *GetDcmtkObject().getDataset(), ignoreTagLength); } bool ParsedDicomFile::LookupTransferSyntax(std::string& result) { +#if 0 + // This was the implementation in Orthanc <= 1.6.1 + // TODO - Shouldn't "dataset.getCurrentXfer()" be used instead of // using the meta header? const char* value = NULL; - assert(pimpl_->file_ != NULL); - if (pimpl_->file_->getMetaInfo() != NULL && - pimpl_->file_->getMetaInfo()->findAndGetString(DCM_TransferSyntaxUID, value).good() && + if (GetDcmtkObject().getMetaInfo() != NULL && + GetDcmtkObject().getMetaInfo()->findAndGetString(DCM_TransferSyntaxUID, value).good() && value != NULL) { result.assign(value); @@ -1647,6 +1673,18 @@ { return false; } +#else + DicomTransferSyntax s; + if (FromDcmtkBridge::LookupOrthancTransferSyntax(s, GetDcmtkObject())) + { + result.assign(GetTransferSyntaxUid(s)); + return true; + } + else + { + return false; + } +#endif } @@ -1655,7 +1693,7 @@ DcmTagKey k(DICOM_TAG_PHOTOMETRIC_INTERPRETATION.GetGroup(), DICOM_TAG_PHOTOMETRIC_INTERPRETATION.GetElement()); - DcmDataset& dataset = *pimpl_->file_->getDataset(); + DcmDataset& dataset = *GetDcmtkObject().getDataset(); const char *c = NULL; if (dataset.findAndGetString(k, c).good() && @@ -1673,6 +1711,6 @@ void ParsedDicomFile::Apply(ITagVisitor& visitor) { - FromDcmtkBridge::Apply(*pimpl_->file_->getDataset(), visitor, GetDefaultDicomEncoding()); + FromDcmtkBridge::Apply(*GetDcmtkObject().getDataset(), visitor, GetDefaultDicomEncoding()); } } diff -r 66879215cbf3 -r 596912ebab5f Core/DicomParsing/ParsedDicomFile.h --- a/Core/DicomParsing/ParsedDicomFile.h Wed May 20 16:38:33 2020 +0200 +++ b/Core/DicomParsing/ParsedDicomFile.h Wed May 20 17:03:24 2020 +0200 @@ -127,6 +127,9 @@ DcmFileFormat& GetDcmtkObject() const; + // The "ParsedDicomFile" object cannot be used after calling this method + DcmFileFormat* ReleaseDcmtkObject(); + ParsedDicomFile* Clone(bool keepSopInstanceUid); #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 diff -r 66879215cbf3 -r 596912ebab5f NEWS --- a/NEWS Wed May 20 16:38:33 2020 +0200 +++ b/NEWS Wed May 20 17:03:24 2020 +0200 @@ -4,12 +4,18 @@ General ------- -* Support of DICOM C-GET SCP +* DICOM transcoding over the REST API +* Transcoding from compressed to uncompressed transfer syntaxes over DICOM + C-STORE SCU (if the remote modality doesn't support compressed syntaxes) +* New configuration options related to transcoding: + "TranscodeDicomProtocol", "BuiltinDecoderTranscoderOrder", + "IngestTranscoding" and "DicomLossyTranscodingQuality" +* Support of DICOM C-GET SCP (contribution by Varian) REST API -------- -* API version has been upgraded to 7 +* API version upgraded to 7 * Improved: - "/instances/../modify": it is now possible to "Keep" the "SOPInstanceUID". Note that it was already possible to "Replace" it. @@ -17,8 +23,11 @@ - "/queries/.../answers/../retrieve": "TargetAet" not mandatory anymore (defaults to the local AET) * Changes: - - "/instances/.../modify", ".../archive", ".../media", - "/tools/create-media" and "/tools/create-archive": New option "Transcode" + - "/{patients|studies|series}/.../modify": New option "KeepSource" + - "/{patients|studies|series|instances}/.../modify": New option "Transcode" + - "/peers/{id}/store": New option "Transcode" + - ".../archive", ".../media", "/tools/create-media" and + "/tools/create-archive": New option "Transcode" - "/ordered-slices": reverted the change introduced in 1.5.8 and go-back to 1.5.7 behaviour. @@ -27,19 +36,29 @@ * New functions in the SDK: - OrthancPluginCreateDicomInstance() + - OrthancPluginCreateMemoryBuffer() + - OrthancPluginEncodeDicomWebJson2() + - OrthancPluginEncodeDicomWebXml2() - OrthancPluginFreeDicomInstance() + - OrthancPluginGetInstanceAdvancedJson() + - OrthancPluginGetInstanceDecodedFrame() + - OrthancPluginGetInstanceDicomWebJson() + - OrthancPluginGetInstanceDicomWebXml() - OrthancPluginGetInstanceFramesCount() - OrthancPluginGetInstanceRawFrame() - - OrthancPluginGetInstanceDecodedFrame() + - OrthancPluginRegisterTranscoderCallback() + - OrthancPluginSerializeDicomInstance() - OrthancPluginTranscodeDicomInstance() - - OrthancPluginSerializeDicomInstance() * "OrthancPluginDicomInstance" structure wrapped in "OrthancPluginCppWrapper.h" +* Allow concurrent calls to the custom image decoders provided by the plugins Maintenance ----------- +* Moved the GDCM sample plugin out of the Orthanc repository as a separate plugin * Fix missing body in "OrthancPluginHttpPost()" and "OrthancPluginHttpPut()" * Fix issue #169 (TransferSyntaxUID change from Explicit to Implicit during C-STORE SCU) +* Fix issue #179 (deadlock in Python plugins) * Upgraded dependencies for static builds (notably on Windows and LSB): - openssl 1.1.1g diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/DefaultDicomImageDecoder.h --- a/OrthancServer/DefaultDicomImageDecoder.h Wed May 20 16:38:33 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,53 +0,0 @@ -/** - * 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 . - **/ - - -#pragma once - -#include "IDicomImageDecoder.h" -#include "../Core/DicomParsing/ParsedDicomFile.h" -#include "../Core/DicomParsing/Internals/DicomImageDecoder.h" - -namespace Orthanc -{ - class DefaultDicomImageDecoder : public IDicomImageDecoder - { - public: - virtual ImageAccessor* Decode(const void* dicom, - size_t size, - unsigned int frame) - { - ParsedDicomFile parsed(dicom, size); - return DicomImageDecoder::Decode(parsed, frame); - } - }; -} diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/OrthancConfiguration.cpp --- a/OrthancServer/OrthancConfiguration.cpp Wed May 20 16:38:33 2020 +0200 +++ b/OrthancServer/OrthancConfiguration.cpp Wed May 20 17:03:24 2020 +0200 @@ -422,8 +422,8 @@ } - std::string OrthancConfiguration::GetStringParameter(const std::string& parameter, - const std::string& defaultValue) const + bool OrthancConfiguration::LookupStringParameter(std::string& target, + const std::string& parameter) const { if (json_.isMember(parameter)) { @@ -434,11 +434,27 @@ } else { - return json_[parameter].asString(); + target = json_[parameter].asString(); + return true; } } else { + return false; + } + } + + + std::string OrthancConfiguration::GetStringParameter(const std::string& parameter, + const std::string& defaultValue) const + { + std::string value; + if (LookupStringParameter(value, parameter)) + { + return value; + } + else + { return defaultValue; } } diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/OrthancConfiguration.h --- a/OrthancServer/OrthancConfiguration.h Wed May 20 16:38:33 2020 +0200 +++ b/OrthancServer/OrthancConfiguration.h Wed May 20 17:03:24 2020 +0200 @@ -163,6 +163,9 @@ fontRegistry_.AddFromResource(resource); } + bool LookupStringParameter(std::string& target, + const std::string& parameter) const; + std::string GetStringParameter(const std::string& parameter, const std::string& defaultValue) const; diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Wed May 20 16:38:33 2020 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Wed May 20 17:03:24 2020 +0200 @@ -130,15 +130,18 @@ if (transcode) { - std::string transcoded; - DicomTransferSyntax sourceSyntax; - bool hasSopInstanceUidChanged; + IDicomTranscoder::DicomImage source; + source.AcquireParsed(*modified); // "modified" is invalid below this point + + IDicomTranscoder::DicomImage transcoded; - if (context.GetTranscoder().TranscodeParsedToBuffer( - transcoded, sourceSyntax, hasSopInstanceUidChanged, - modified->GetDcmtkObject(), targetSyntax, true)) + std::set s; + s.insert(targetSyntax); + + if (context.Transcode(transcoded, source, s, true)) { - call.GetOutput().AnswerBuffer(transcoded, MimeType_Dicom); + call.GetOutput().AnswerBuffer(transcoded.GetBufferData(), + transcoded.GetBufferSize(), MimeType_Dicom); } else { @@ -179,9 +182,10 @@ modification.SetLevel(ResourceType_Instance); } - if (request.isMember("Transcode")) + static const char* TRANSCODE = "Transcode"; + if (request.isMember(TRANSCODE)) { - std::string s = SerializationToolbox::ReadString(request, "Transcode"); + std::string s = SerializationToolbox::ReadString(request, TRANSCODE); DicomTransferSyntax syntax; if (LookupTransferSyntax(syntax, s)) @@ -214,6 +218,17 @@ } + static void SetKeepSource(CleaningInstancesJob& job, + const Json::Value& body) + { + static const char* KEEP_SOURCE = "KeepSource"; + if (body.isMember(KEEP_SOURCE)) + { + job.SetKeepSource(SerializationToolbox::ReadBoolean(body, KEEP_SOURCE)); + } + } + + static void SubmitModificationJob(std::unique_ptr& modification, bool isAnonymization, RestApiPostCall& call, @@ -223,11 +238,19 @@ ServerContext& context = OrthancRestApi::GetContext(call); std::unique_ptr job(new ResourceModificationJob(context)); - + job->SetModification(modification.release(), level, isAnonymization); job->SetOrigin(call); + SetKeepSource(*job, body); + + static const char* TRANSCODE = "Transcode"; + if (body.isMember(TRANSCODE)) + { + job->SetTranscode(SerializationToolbox::ReadString(body, TRANSCODE)); + } context.AddChildInstances(*job, call.GetUriComponent("id", "")); + job->AddTrailingStep(); OrthancRestApi::GetApi(call).SubmitCommandsJob (call, job.release(), true /* synchronous by default */, body); @@ -723,14 +746,10 @@ { job->AddSourceSeries(series[i]); } - + job->AddTrailingStep(); - static const char* KEEP_SOURCE = "KeepSource"; - if (request.isMember(KEEP_SOURCE)) - { - job->SetKeepSource(SerializationToolbox::ReadBoolean(request, KEEP_SOURCE)); - } + SetKeepSource(*job, request); static const char* REMOVE = "Remove"; if (request.isMember(REMOVE)) @@ -809,11 +828,7 @@ job->AddTrailingStep(); - static const char* KEEP_SOURCE = "KeepSource"; - if (request.isMember(KEEP_SOURCE)) - { - job->SetKeepSource(SerializationToolbox::ReadBoolean(request, KEEP_SOURCE)); - } + SetKeepSource(*job, request); OrthancRestApi::GetApi(call).SubmitCommandsJob (call, job.release(), true /* synchronous by default */, request); diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/OrthancRestApi/OrthancRestModalities.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Wed May 20 16:38:33 2020 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Wed May 20 17:03:24 2020 +0200 @@ -1136,21 +1136,31 @@ std::unique_ptr job(new OrthancPeerStoreJob(context)); GetInstancesToExport(request, *job, remote, call); + + static const char* TRANSCODE = "Transcode"; + if (request.type() == Json::objectValue && + request.isMember(TRANSCODE)) + { + job->SetTranscode(SerializationToolbox::ReadString(request, TRANSCODE)); + } - OrthancConfiguration::ReaderLock lock; - - WebServiceParameters peer; - if (lock.GetConfiguration().LookupOrthancPeer(peer, remote)) { - job->SetPeer(peer); - OrthancRestApi::GetApi(call).SubmitCommandsJob - (call, job.release(), true /* synchronous by default */, request); + OrthancConfiguration::ReaderLock lock; + + WebServiceParameters peer; + if (lock.GetConfiguration().LookupOrthancPeer(peer, remote)) + { + job->SetPeer(peer); + } + else + { + throw OrthancException(ErrorCode_UnknownResource, + "No peer with symbolic name: " + remote); + } } - else - { - throw OrthancException(ErrorCode_UnknownResource, - "No peer with symbolic name: " + remote); - } + + OrthancRestApi::GetApi(call).SubmitCommandsJob + (call, job.release(), true /* synchronous by default */, request); } static void PeerSystem(RestApiGetCall& call) diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Wed May 20 16:38:33 2020 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Wed May 20 17:03:24 2020 +0200 @@ -43,7 +43,7 @@ #include "../../Core/Images/Image.h" #include "../../Core/Images/ImageProcessing.h" #include "../../Core/Logging.h" -#include "../DefaultDicomImageDecoder.h" +#include "../../Core/MultiThreading/Semaphore.h" #include "../OrthancConfiguration.h" #include "../Search/DatabaseLookup.h" #include "../ServerContext.h" @@ -56,6 +56,14 @@ #include +/** + * 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, @@ -547,44 +555,23 @@ { std::string publicId = call.GetUriComponent("id", ""); -#if ORTHANC_ENABLE_PLUGINS == 1 - if (context.GetPlugins().HasCustomImageDecoder()) - { - // TODO create a cache of file - std::string dicomContent; - context.ReadDicom(dicomContent, publicId); - decoded.reset(context.GetPlugins().DecodeUnsafe(dicomContent.c_str(), dicomContent.size(), frame)); - - /** - * Note that we call "DecodeUnsafe()": We do not fallback to - * the builtin decoder if no installed decoder plugin is able - * to decode the image. This allows us to take advantage of - * the cache below. - **/ - - if (handler.RequiresDicomTags() && - decoded.get() != NULL) - { - // TODO Optimize this lookup for photometric interpretation: - // It should be implemented by the plugin to avoid parsing - // twice the DICOM file - ParsedDicomFile parsed(dicomContent); - parsed.ExtractDicomSummary(dicom); - } - } -#endif + decoded.reset(context.DecodeDicomFrame(publicId, frame)); if (decoded.get() == NULL) { - // Use Orthanc's built-in decoder, using the cache to speed-up - // things on multi-frame images - ServerContext::DicomCacheLocker locker(context, publicId); - decoded.reset(DicomImageDecoder::Decode(locker.GetDicom(), frame)); - - if (handler.RequiresDicomTags()) - { - locker.GetDicom().ExtractDicomSummary(dicom); - } + 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) @@ -938,6 +925,8 @@ template static void GetImage(RestApiGetCall& call) { + Semaphore::Locker locker(throttlingSemaphore_); + GetImageHandler handler(mode); IDecodedFrameHandler::Apply(call, handler); } @@ -945,6 +934,8 @@ static void GetRenderedFrame(RestApiGetCall& call) { + Semaphore::Locker locker(throttlingSemaphore_); + RenderedFrameHandler handler; IDecodedFrameHandler::Apply(call, handler); } @@ -952,6 +943,8 @@ static void GetMatlabImage(RestApiGetCall& call) { + Semaphore::Locker locker(throttlingSemaphore_); + ServerContext& context = OrthancRestApi::GetContext(call); std::string frameId = call.GetUriComponent("frame", "0"); @@ -967,21 +960,19 @@ } std::string publicId = call.GetUriComponent("id", ""); - std::string dicomContent; - context.ReadDicom(dicomContent, publicId); + std::unique_ptr decoded(context.DecodeDicomFrame(publicId, frame)); -#if ORTHANC_ENABLE_PLUGINS == 1 - IDicomImageDecoder& decoder = context.GetPlugins(); -#else - DefaultDicomImageDecoder decoder; // This is Orthanc's built-in decoder -#endif - - std::unique_ptr decoded(decoder.Decode(dicomContent.c_str(), dicomContent.size(), frame)); - - std::string result; - decoded->ToMatlabString(result); - - call.GetOutput().AnswerBuffer(result, MimeType_PlainText); + 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); + } } diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Wed May 20 16:38:33 2020 +0200 +++ b/OrthancServer/ServerContext.cpp Wed May 20 17:03:24 2020 +0200 @@ -34,6 +34,7 @@ #include "PrecompiledHeadersServer.h" #include "ServerContext.h" +#include "../Core/DicomParsing/Internals/DicomImageDecoder.h" #include "../Core/Cache/SharedArchive.h" #include "../Core/DicomParsing/DcmtkTranscoder.h" #include "../Core/DicomParsing/FromDcmtkBridge.h" @@ -83,7 +84,7 @@ { const ServerIndexChange& change = dynamic_cast(*obj.get()); - boost::recursive_mutex::scoped_lock lock(that->listenersMutex_); + boost::shared_lock lock(that->listenersMutex_); for (ServerListeners::iterator it = that->listeners_.begin(); it != that->listeners_.end(); ++it) { @@ -245,36 +246,72 @@ isHttpServerSecure_(true), isExecuteLuaEnabled_(false), overwriteInstances_(false), - dcmtkTranscoder_(new DcmtkTranscoder) + dcmtkTranscoder_(new DcmtkTranscoder), + isIngestTranscoding_(false) { + try { - OrthancConfiguration::ReaderLock lock; + unsigned int lossyQuality; + + { + OrthancConfiguration::ReaderLock lock; - queryRetrieveArchive_.reset( - new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("QueryRetrieveSize", 100))); - mediaArchive_.reset( - new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize", 1))); - defaultLocalAet_ = lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC"); - jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs", 2)); - saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs", true); - metricsRegistry_->SetEnabled(lock.GetConfiguration().GetBooleanParameter("MetricsEnabled", true)); + queryRetrieveArchive_.reset( + new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("QueryRetrieveSize", 100))); + mediaArchive_.reset( + new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize", 1))); + defaultLocalAet_ = lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC"); + jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs", 2)); + saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs", true); + metricsRegistry_->SetEnabled(lock.GetConfiguration().GetBooleanParameter("MetricsEnabled", true)); + + // New configuration options in Orthanc 1.5.1 + findStorageAccessMode_ = StringToFindStorageAccessMode(lock.GetConfiguration().GetStringParameter("StorageAccessOnFind", "Always")); + limitFindInstances_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0); + limitFindResults_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0); + + // New configuration option in Orthanc 1.6.0 + storageCommitmentReports_.reset(new StorageCommitmentReports(lock.GetConfiguration().GetUnsignedIntegerParameter("StorageCommitmentReportsSize", 100))); + + // New options in Orthanc 1.7.0 + transcodeDicomProtocol_ = lock.GetConfiguration().GetBooleanParameter("TranscodeDicomProtocol", true); + builtinDecoderTranscoderOrder_ = StringToBuiltinDecoderTranscoderOrder(lock.GetConfiguration().GetStringParameter("BuiltinDecoderTranscoderOrder", "After")); + lossyQuality = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomLossyTranscodingQuality", 90); - // New configuration options in Orthanc 1.5.1 - findStorageAccessMode_ = StringToFindStorageAccessMode(lock.GetConfiguration().GetStringParameter("StorageAccessOnFind", "Always")); - limitFindInstances_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0); - limitFindResults_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0); - - // New configuration option in Orthanc 1.6.0 - storageCommitmentReports_.reset(new StorageCommitmentReports(lock.GetConfiguration().GetUnsignedIntegerParameter("StorageCommitmentReportsSize", 100))); + std::string s; + if (lock.GetConfiguration().LookupStringParameter(s, "IngestTranscoding")) + { + if (LookupTransferSyntax(ingestTransferSyntax_, s)) + { + isIngestTranscoding_ = true; + LOG(WARNING) << "Incoming DICOM instances will automatically be transcoded to " + << "transfer syntax: " << GetTransferSyntaxUid(ingestTransferSyntax_); + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Unknown transfer syntax for ingest transcoding: " + s); + } + } + else + { + isIngestTranscoding_ = false; + LOG(INFO) << "Automated transcoding of incoming DICOM instances is disabled"; + } + } - // New option in Orthanc 1.7.0 - transcodeDicomProtocol_ = lock.GetConfiguration().GetBooleanParameter("TranscodeDicomProtocol", true); - } + jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200); - jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200); - - listeners_.push_back(ServerListener(luaListener_, "Lua")); - changeThread_ = boost::thread(ChangeThread, this, (unitTesting ? 20 : 100)); + listeners_.push_back(ServerListener(luaListener_, "Lua")); + changeThread_ = boost::thread(ChangeThread, this, (unitTesting ? 20 : 100)); + + dynamic_cast(*dcmtkTranscoder_).SetLossyQuality(lossyQuality); + } + catch (OrthancException&) + { + Stop(); + throw; + } } @@ -294,7 +331,7 @@ if (!done_) { { - boost::recursive_mutex::scoped_lock lock(listenersMutex_); + boost::unique_lock lock(listenersMutex_); listeners_.clear(); } @@ -381,7 +418,7 @@ bool accepted = true; { - boost::recursive_mutex::scoped_lock lock(listenersMutex_); + boost::shared_lock lock(listenersMutex_); for (ServerListeners::iterator it = listeners_.begin(); it != listeners_.end(); ++it) { @@ -471,7 +508,7 @@ if (status == StoreStatus_Success || status == StoreStatus_AlreadyStored) { - boost::recursive_mutex::scoped_lock lock(listenersMutex_); + boost::shared_lock lock(listenersMutex_); for (ServerListeners::iterator it = listeners_.begin(); it != listeners_.end(); ++it) { @@ -506,21 +543,19 @@ DicomInstanceToStore& dicom, StoreInstanceMode mode) { - //const DicomTransferSyntax option = DicomTransferSyntax_JPEGProcess1; - const DicomTransferSyntax option = DicomTransferSyntax_LittleEndianExplicit; - - if (1) + if (!isIngestTranscoding_) { + // No automated transcoding. This was the only path in Orthanc <= 1.6.1. return StoreAfterTranscoding(resultPublicId, dicom, mode); } else { - // TODO => Automated transcoding of incoming DICOM files - + // Automated transcoding of incoming DICOM files + DicomTransferSyntax sourceSyntax; if (!FromDcmtkBridge::LookupOrthancTransferSyntax( sourceSyntax, dicom.GetParsedDicomFile().GetDcmtkObject()) || - sourceSyntax == option) + sourceSyntax == ingestTransferSyntax_) { // No transcoding return StoreAfterTranscoding(resultPublicId, dicom, mode); @@ -528,31 +563,30 @@ else { std::set syntaxes; - syntaxes.insert(option); + syntaxes.insert(ingestTransferSyntax_); - std::unique_ptr transcoded( - GetTranscoder().TranscodeToParsed(dicom.GetParsedDicomFile().GetDcmtkObject(), - dicom.GetBufferData(), dicom.GetBufferSize(), - syntaxes, true /* allow new SOP instance UID */)); + IDicomTranscoder::DicomImage source; + source.SetExternalBuffer(dicom.GetBufferData(), dicom.GetBufferSize()); - if (transcoded.get() == NULL) + IDicomTranscoder::DicomImage transcoded; + if (Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */)) { - // Cannot transcode => store the original file - return StoreAfterTranscoding(resultPublicId, dicom, mode); - } - else - { - std::unique_ptr tmp( - ParsedDicomFile::AcquireDcmtkObject(transcoded->ReleaseDicom())); - + std::unique_ptr tmp(transcoded.ReleaseAsParsedDicomFile()); + DicomInstanceToStore toStore; toStore.SetParsedDicomFile(*tmp); toStore.SetOrigin(dicom.GetOrigin()); StoreStatus ok = StoreAfterTranscoding(resultPublicId, toStore, mode); - printf(">> %s\n", resultPublicId.c_str()); + assert(resultPublicId == tmp->GetHasher().HashInstance()); + return ok; } + else + { + // Cannot transcode => store the original file + return StoreAfterTranscoding(resultPublicId, dicom, mode); + } } } } @@ -822,7 +856,7 @@ #if ORTHANC_ENABLE_PLUGINS == 1 void ServerContext::SetPlugins(OrthancPlugins& plugins) { - boost::recursive_mutex::scoped_lock lock(listenersMutex_); + boost::unique_lock lock(listenersMutex_); plugins_ = &plugins; @@ -835,7 +869,7 @@ void ServerContext::ResetPlugins() { - boost::recursive_mutex::scoped_lock lock(listenersMutex_); + boost::unique_lock lock(listenersMutex_); plugins_ = NULL; @@ -1171,26 +1205,95 @@ } - IDicomTranscoder& ServerContext::GetTranscoder() + ImageAccessor* ServerContext::DecodeDicomFrame(const std::string& publicId, + unsigned int frameIndex) { - IDicomTranscoder* transcoder = dcmtkTranscoder_.get(); + if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before) + { + // Use Orthanc's built-in decoder, using the cache to speed-up + // things on multi-frame images + ServerContext::DicomCacheLocker locker(*this, publicId); + std::unique_ptr decoded( + DicomImageDecoder::Decode(locker.GetDicom(), frameIndex)); + if (decoded.get() != NULL) + { + return decoded.release(); + } + } #if ORTHANC_ENABLE_PLUGINS == 1 - if (HasPlugins()) + if (HasPlugins() && + GetPlugins().HasCustomImageDecoder()) { - transcoder = &GetPlugins(); + // TODO: Store the raw buffer in the DicomCacheLocker + std::string dicomContent; + ReadDicom(dicomContent, publicId); + std::unique_ptr decoded( + GetPlugins().Decode(dicomContent.c_str(), dicomContent.size(), frameIndex)); + if (decoded.get() != NULL) + { + return decoded.release(); + } + else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) + { + LOG(INFO) << "The installed image decoding plugins cannot handle an image, " + << "fallback to the built-in DCMTK decoder"; + } } #endif - if (transcoder == NULL) + if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) { - throw OrthancException(ErrorCode_InternalError); + ServerContext::DicomCacheLocker locker(*this, publicId); + return DicomImageDecoder::Decode(locker.GetDicom(), frameIndex); } else { - return *transcoder; + return NULL; // Built-in decoder is disabled + } + } + + + ImageAccessor* ServerContext::DecodeDicomFrame(const DicomInstanceToStore& dicom, + unsigned int frameIndex) + { + if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before) + { + std::unique_ptr decoded( + DicomImageDecoder::Decode(dicom.GetParsedDicomFile(), frameIndex)); + if (decoded.get() != NULL) + { + return decoded.release(); + } } - } + +#if ORTHANC_ENABLE_PLUGINS == 1 + if (HasPlugins() && + GetPlugins().HasCustomImageDecoder()) + { + std::unique_ptr decoded( + GetPlugins().Decode(dicom.GetBufferData(), dicom.GetBufferSize(), frameIndex)); + if (decoded.get() != NULL) + { + return decoded.release(); + } + else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) + { + LOG(INFO) << "The installed image decoding plugins cannot handle an image, " + << "fallback to the built-in DCMTK decoder"; + } + } +#endif + + if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) + { + return DicomImageDecoder::Decode(dicom.GetParsedDicomFile(), frameIndex); + } + else + { + return NULL; + } + } void ServerContext::StoreWithTranscoding(std::string& sopClassUid, @@ -1211,8 +1314,48 @@ } else { - connection.Transcode(sopClassUid, sopInstanceUid, GetTranscoder(), data, dicom.size(), + connection.Transcode(sopClassUid, sopInstanceUid, *this, data, dicom.size(), hasMoveOriginator, moveOriginatorAet, moveOriginatorId); } } + + + bool ServerContext::Transcode(DicomImage& target, + DicomImage& source /* in, "GetParsed()" possibly modified */, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) + { + if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before) + { + if (dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid)) + { + return true; + } + } + +#if ORTHANC_ENABLE_PLUGINS == 1 + if (HasPlugins() && + GetPlugins().HasCustomTranscoder()) + { + if (GetPlugins().Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid)) + { + return true; + } + else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) + { + LOG(INFO) << "The installed transcoding plugins cannot handle an image, " + << "fallback to the built-in DCMTK transcoder"; + } + } +#endif + + if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) + { + return dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid); + } + else + { + return false; + } + } } diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/ServerContext.h --- a/OrthancServer/ServerContext.h Wed May 20 16:38:33 2020 +0200 +++ b/OrthancServer/ServerContext.h Wed May 20 17:03:24 2020 +0200 @@ -65,6 +65,7 @@ **/ class ServerContext : public IStorageCommitmentFactory, + public IDicomTranscoder, private JobsRegistry::IObserver { public: @@ -202,7 +203,7 @@ #endif ServerListeners listeners_; - boost::recursive_mutex listenersMutex_; + boost::shared_mutex listenersMutex_; bool done_; bool haveJobsChanged_; @@ -228,6 +229,9 @@ bool transcodeDicomProtocol_; std::unique_ptr dcmtkTranscoder_; + BuiltinDecoderTranscoderOrder builtinDecoderTranscoderOrder_; + bool isIngestTranscoding_; + DicomTransferSyntax ingestTransferSyntax_; StoreStatus StoreAfterTranscoding(std::string& resultPublicId, DicomInstanceToStore& dicom, @@ -459,6 +463,12 @@ return *storageCommitmentReports_; } + ImageAccessor* DecodeDicomFrame(const std::string& publicId, + unsigned int frameIndex); + + ImageAccessor* DecodeDicomFrame(const DicomInstanceToStore& dicom, + unsigned int frameIndex); + void StoreWithTranscoding(std::string& sopClassUid, std::string& sopInstanceUid, DicomStoreUserConnection& connection, @@ -467,8 +477,11 @@ const std::string& moveOriginatorAet, uint16_t moveOriginatorId); - // This accessor can be used even if the global option + // This method can be used even if the global option // "TranscodeDicomProtocol" is set to "false" - IDicomTranscoder& GetTranscoder(); + virtual bool Transcode(DicomImage& target, + DicomImage& source /* in, "GetParsed()" possibly modified */, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; }; } diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/ServerEnumerations.cpp --- a/OrthancServer/ServerEnumerations.cpp Wed May 20 16:38:33 2020 +0200 +++ b/OrthancServer/ServerEnumerations.cpp Wed May 20 17:03:24 2020 +0200 @@ -214,6 +214,29 @@ "should be \"Always\", \"Never\" or \"Answers\": " + value); } } + + + BuiltinDecoderTranscoderOrder StringToBuiltinDecoderTranscoderOrder(const std::string& value) + { + if (value == "Before") + { + return BuiltinDecoderTranscoderOrder_Before; + } + else if (value == "After") + { + return BuiltinDecoderTranscoderOrder_After; + } + else if (value == "Disabled") + { + return BuiltinDecoderTranscoderOrder_Disabled; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Configuration option \"BuiltinDecoderTranscoderOrder\" " + "should be \"After\", \"Before\" or \"Disabled\": " + value); + } + } std::string GetBasePath(ResourceType type, diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/ServerEnumerations.h --- a/OrthancServer/ServerEnumerations.h Wed May 20 16:38:33 2020 +0200 +++ b/OrthancServer/ServerEnumerations.h Wed May 20 17:03:24 2020 +0200 @@ -175,6 +175,13 @@ ChangeType_NewChildInstance = 4097 }; + enum BuiltinDecoderTranscoderOrder + { + BuiltinDecoderTranscoderOrder_Before, + BuiltinDecoderTranscoderOrder_After, + BuiltinDecoderTranscoderOrder_Disabled + }; + void InitializeServerEnumerations(); @@ -194,6 +201,8 @@ FindStorageAccessMode StringToFindStorageAccessMode(const std::string& str); + BuiltinDecoderTranscoderOrder StringToBuiltinDecoderTranscoderOrder(const std::string& str); + std::string EnumerationToString(FileContentType type); std::string GetFileContentMime(FileContentType type); diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/ServerJobs/ArchiveJob.cpp --- a/OrthancServer/ServerJobs/ArchiveJob.cpp Wed May 20 16:38:33 2020 +0200 +++ b/OrthancServer/ServerJobs/ArchiveJob.cpp Wed May 20 17:03:24 2020 +0200 @@ -442,28 +442,17 @@ // New in Orthanc 1.7.0 std::set syntaxes; syntaxes.insert(transferSyntax); - - parsed.reset(new ParsedDicomFile(content)); - const char* data = content.empty() ? NULL : content.c_str(); - - std::unique_ptr transcodedDicom( - context.GetTranscoder().TranscodeToParsed( - parsed->GetDcmtkObject(), data, content.size(), - syntaxes, true /* allow new SOP instance UID */)); + + IDicomTranscoder::DicomImage source, transcoded; + source.SetExternalBuffer(content); - if (transcodedDicom.get() != NULL && - transcodedDicom->GetDicom().getDataset() != NULL) + if (context.Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */)) { - std::string transcoded; - FromDcmtkBridge::SaveToMemoryBuffer( - transcoded, *transcodedDicom->GetDicom().getDataset()); - - writer.Write(transcoded); + writer.Write(transcoded.GetBufferData(), transcoded.GetBufferSize()); if (dicomDir != NULL) { - std::unique_ptr tmp( - ParsedDicomFile::AcquireDcmtkObject(transcodedDicom->ReleaseDicom())); + std::unique_ptr tmp(transcoded.ReleaseAsParsedDicomFile()); dicomDir->Add(dicomDirFolder, filename_, *tmp); } diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/ServerJobs/CleaningInstancesJob.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/CleaningInstancesJob.cpp Wed May 20 17:03:24 2020 +0200 @@ -0,0 +1,120 @@ +/** + * 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 . + **/ + + +#include "CleaningInstancesJob.h" + +#include "../../Core/SerializationToolbox.h" +#include "../ServerContext.h" + + +namespace Orthanc +{ + bool CleaningInstancesJob::HandleTrailingStep() + { + if (!keepSource_) + { + const size_t n = GetInstancesCount(); + + for (size_t i = 0; i < n; i++) + { + Json::Value tmp; + context_.DeleteResource(tmp, GetInstance(i), ResourceType_Instance); + } + } + + return true; + } + + + void CleaningInstancesJob::SetKeepSource(bool keep) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + keepSource_ = keep; + } + + + static const char* KEEP_SOURCE = "KeepSource"; + + + CleaningInstancesJob::CleaningInstancesJob(ServerContext& context, + const Json::Value& serialized, + bool defaultKeepSource) : + SetOfInstancesJob(serialized), // (*) + context_(context) + { + if (!HasTrailingStep()) + { + // Should have been set by (*) + throw OrthancException(ErrorCode_InternalError); + } + + if (serialized.isMember(KEEP_SOURCE)) + { + keepSource_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOURCE); + } + else + { + keepSource_ = defaultKeepSource; + } + } + + + bool CleaningInstancesJob::Serialize(Json::Value& target) + { + if (!SetOfInstancesJob::Serialize(target)) + { + return false; + } + else + { + target[KEEP_SOURCE] = keepSource_; + return true; + } + } + + + void CleaningInstancesJob::Start() + { + if (!HasTrailingStep()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "AddTrailingStep() should have been called before submitting the job"); + } + + SetOfInstancesJob::Start(); + } +} diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/ServerJobs/CleaningInstancesJob.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/CleaningInstancesJob.h Wed May 20 17:03:24 2020 +0200 @@ -0,0 +1,79 @@ +/** + * 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 . + **/ + + +#pragma once + +#include "../../Core/JobsEngine/SetOfInstancesJob.h" + +namespace Orthanc +{ + class ServerContext; + + class CleaningInstancesJob : public SetOfInstancesJob + { + private: + ServerContext& context_; + bool keepSource_; + + protected: + virtual bool HandleTrailingStep(); + + public: + CleaningInstancesJob(ServerContext& context, + bool keepSource) : + context_(context), + keepSource_(keepSource) + { + } + + CleaningInstancesJob(ServerContext& context, + const Json::Value& serialized, + bool defaultKeepSource); + + ServerContext& GetContext() const + { + return context_; + } + + bool IsKeepSource() const + { + return keepSource_; + } + + void SetKeepSource(bool keep); + + virtual bool Serialize(Json::Value& target); + + virtual void Start(); + }; +} diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/ServerJobs/MergeStudyJob.cpp --- a/OrthancServer/ServerJobs/MergeStudyJob.cpp Wed May 20 16:38:33 2020 +0200 +++ b/OrthancServer/ServerJobs/MergeStudyJob.cpp Wed May 20 17:03:24 2020 +0200 @@ -48,7 +48,7 @@ // Add all the instances of the series as to be processed std::list instances; - context_.GetIndex().GetChildren(instances, series); + GetContext().GetIndex().GetChildren(instances, series); for (std::list::const_iterator it = instances.begin(); it != instances.end(); ++it) @@ -68,7 +68,7 @@ else { std::list series; - context_.GetIndex().GetChildren(series, study); + GetContext().GetIndex().GetChildren(series, study); for (std::list::const_iterator it = series.begin(); it != series.end(); ++it) @@ -81,6 +81,12 @@ bool MergeStudyJob::HandleInstance(const std::string& instance) { + if (!HasTrailingStep()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "AddTrailingStep() should have been called after AddSourceXXX()"); + } + /** * Retrieve the DICOM instance to be modified **/ @@ -89,7 +95,7 @@ try { - ServerContext::DicomCacheLocker locker(context_, instance); + ServerContext::DicomCacheLocker locker(GetContext(), instance); modified.reset(locker.GetDicom().Clone(true)); } catch (OrthancException&) @@ -145,7 +151,7 @@ toStore.SetParsedDicomFile(*modified); std::string modifiedInstance; - if (context_.Store(modifiedInstance, toStore, + if (GetContext().Store(modifiedInstance, toStore, StoreInstanceMode_Default) != StoreStatus_Success) { LOG(ERROR) << "Error while storing a modified instance " << instance; @@ -156,27 +162,9 @@ } - bool MergeStudyJob::HandleTrailingStep() - { - if (!keepSource_) - { - const size_t n = GetInstancesCount(); - - for (size_t i = 0; i < n; i++) - { - Json::Value tmp; - context_.DeleteResource(tmp, GetInstance(i), ResourceType_Instance); - } - } - - return true; - } - - MergeStudyJob::MergeStudyJob(ServerContext& context, const std::string& targetStudy) : - context_(context), - keepSource_(false), + CleaningInstancesJob(context, false /* by default, remove source instances */), targetStudy_(targetStudy) { /** @@ -185,7 +173,7 @@ ResourceType type; - if (!context_.GetIndex().LookupResourceType(type, targetStudy) || + if (!GetContext().GetIndex().LookupResourceType(type, targetStudy) || type != ResourceType_Study) { throw OrthancException(ErrorCode_UnknownResource, @@ -202,7 +190,7 @@ DicomTag::AddTagsForModule(removals_, DicomModule_Study); std::list instances; - context_.GetIndex().GetChildInstances(instances, targetStudy); + GetContext().GetIndex().GetChildInstances(instances, targetStudy); if (instances.empty()) { @@ -212,7 +200,7 @@ DicomMap dicom; { - ServerContext::DicomCacheLocker locker(context_, instances.front()); + ServerContext::DicomCacheLocker locker(GetContext(), instances.front()); locker.GetDicom().ExtractDicomSummary(dicom); } @@ -260,7 +248,7 @@ { throw OrthancException(ErrorCode_BadSequenceOfCalls); } - else if (!context_.GetIndex().LookupResourceType(level, studyOrSeries)) + else if (!GetContext().GetIndex().LookupResourceType(level, studyOrSeries)) { throw OrthancException(ErrorCode_UnknownResource, "Cannot find this resource: " + studyOrSeries); @@ -295,7 +283,7 @@ { throw OrthancException(ErrorCode_BadSequenceOfCalls); } - else if (!context_.GetIndex().LookupParent(parent, series, ResourceType_Study)) + else if (!GetContext().GetIndex().LookupParent(parent, series, ResourceType_Study)) { throw OrthancException(ErrorCode_UnknownResource, "This resource is not a series: " + series); @@ -321,7 +309,7 @@ { throw OrthancException(ErrorCode_BadSequenceOfCalls); } - else if (!context_.GetIndex().LookupResourceType(actualLevel, study) || + else if (!GetContext().GetIndex().LookupResourceType(actualLevel, study) || actualLevel != ResourceType_Study) { throw OrthancException(ErrorCode_UnknownResource, @@ -334,25 +322,13 @@ } - void MergeStudyJob::SetKeepSource(bool keep) - { - if (IsStarted()) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - - keepSource_ = keep; - } - - void MergeStudyJob::GetPublicContent(Json::Value& value) { - SetOfInstancesJob::GetPublicContent(value); + CleaningInstancesJob::GetPublicContent(value); value["TargetStudy"] = targetStudy_; } - static const char* KEEP_SOURCE = "KeepSource"; static const char* TARGET_STUDY = "TargetStudy"; static const char* REPLACEMENTS = "Replacements"; static const char* REMOVALS = "Removals"; @@ -362,8 +338,8 @@ MergeStudyJob::MergeStudyJob(ServerContext& context, const Json::Value& serialized) : - SetOfInstancesJob(serialized), // (*) - context_(context) + CleaningInstancesJob(context, serialized, + false /* by default, remove source instances */) // (*) { if (!HasTrailingStep()) { @@ -371,7 +347,6 @@ throw OrthancException(ErrorCode_InternalError); } - keepSource_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOURCE); targetStudy_ = SerializationToolbox::ReadString(serialized, TARGET_STUDY); SerializationToolbox::ReadMapOfTags(replacements_, serialized, REPLACEMENTS); SerializationToolbox::ReadSetOfTags(removals_, serialized, REMOVALS); @@ -382,13 +357,12 @@ bool MergeStudyJob::Serialize(Json::Value& target) { - if (!SetOfInstancesJob::Serialize(target)) + if (!CleaningInstancesJob::Serialize(target)) { return false; } else { - target[KEEP_SOURCE] = keepSource_; target[TARGET_STUDY] = targetStudy_; SerializationToolbox::WriteMapOfTags(target, replacements_, REPLACEMENTS); SerializationToolbox::WriteSetOfTags(target, removals_, REMOVALS); diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/ServerJobs/MergeStudyJob.h --- a/OrthancServer/ServerJobs/MergeStudyJob.h Wed May 20 16:38:33 2020 +0200 +++ b/OrthancServer/ServerJobs/MergeStudyJob.h Wed May 20 17:03:24 2020 +0200 @@ -34,22 +34,20 @@ #pragma once #include "../../Core/DicomFormat/DicomMap.h" -#include "../../Core/JobsEngine/SetOfInstancesJob.h" #include "../DicomInstanceOrigin.h" +#include "CleaningInstancesJob.h" namespace Orthanc { class ServerContext; - class MergeStudyJob : public SetOfInstancesJob + class MergeStudyJob : public CleaningInstancesJob { private: typedef std::map SeriesUidMap; typedef std::map Replacements; - ServerContext& context_; - bool keepSource_; std::string targetStudy_; Replacements replacements_; std::set removals_; @@ -61,12 +59,9 @@ void AddSourceStudyInternal(const std::string& study); - protected: virtual bool HandleInstance(const std::string& instance); - virtual bool HandleTrailingStep(); - public: MergeStudyJob(ServerContext& context, const std::string& targetStudy); @@ -85,13 +80,6 @@ void AddSourceSeries(const std::string& series); - bool IsKeepSource() const - { - return keepSource_; - } - - void SetKeepSource(bool keep); - void SetOrigin(const DicomInstanceOrigin& origin); void SetOrigin(const RestApiCall& call); diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp --- a/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp Wed May 20 16:38:33 2020 +0200 +++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp Wed May 20 17:03:24 2020 +0200 @@ -35,8 +35,11 @@ #include "OrthancPeerStoreJob.h" #include "../../Core/Logging.h" +#include "../../Core/SerializationToolbox.h" #include "../ServerContext.h" +#include + namespace Orthanc { @@ -55,7 +58,31 @@ try { - context_.ReadDicom(client_->GetBody(), instance); + if (transcode_) + { + std::string dicom; + context_.ReadDicom(dicom, instance); + + std::set syntaxes; + syntaxes.insert(transferSyntax_); + + IDicomTranscoder::DicomImage source, transcoded; + source.SetExternalBuffer(dicom); + + if (context_.Transcode(transcoded, source, syntaxes, true)) + { + client_->GetBody().assign(reinterpret_cast(transcoded.GetBufferData()), + transcoded.GetBufferSize()); + } + else + { + client_->GetBody().swap(dicom); + } + } + else + { + context_.ReadDicom(client_->GetBody(), instance); + } } catch (OrthancException& e) { @@ -94,6 +121,61 @@ } + DicomTransferSyntax OrthancPeerStoreJob::GetTransferSyntax() const + { + if (transcode_) + { + return transferSyntax_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void OrthancPeerStoreJob::SetTranscode(DicomTransferSyntax syntax) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + transcode_ = true; + transferSyntax_ = syntax; + } + } + + + void OrthancPeerStoreJob::SetTranscode(const std::string& transferSyntaxUid) + { + DicomTransferSyntax s; + if (LookupTransferSyntax(s, transferSyntaxUid)) + { + SetTranscode(s); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, + "Unknown transfer syntax UID: " + transferSyntaxUid); + } + } + + + void OrthancPeerStoreJob::ClearTranscode() + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + transcode_ = false; + } + } + + void OrthancPeerStoreJob::Stop(JobStopReason reason) // For pausing jobs { client_.reset(NULL); @@ -109,17 +191,33 @@ false /* allow simple format if possible */, false /* don't include passwords */); value["Peer"] = v; + + if (transcode_) + { + value["Transcode"] = GetTransferSyntaxUid(transferSyntax_); + } } static const char* PEER = "Peer"; + static const char* TRANSCODE = "Transcode"; OrthancPeerStoreJob::OrthancPeerStoreJob(ServerContext& context, const Json::Value& serialized) : SetOfInstancesJob(serialized), context_(context) { + assert(serialized.type() == Json::objectValue); peer_ = WebServiceParameters(serialized[PEER]); + + if (serialized.isMember(TRANSCODE)) + { + SetTranscode(SerializationToolbox::ReadString(serialized, TRANSCODE)); + } + else + { + transcode_ = false; + } } @@ -131,9 +229,16 @@ } else { + assert(target.type() == Json::objectValue); peer_.Serialize(target[PEER], true /* force advanced format */, true /* include passwords */); + + if (transcode_) + { + target[TRANSCODE] = GetTransferSyntaxUid(transferSyntax_); + } + return true; } } diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/ServerJobs/OrthancPeerStoreJob.h --- a/OrthancServer/ServerJobs/OrthancPeerStoreJob.h Wed May 20 16:38:33 2020 +0200 +++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.h Wed May 20 17:03:24 2020 +0200 @@ -48,6 +48,8 @@ ServerContext& context_; WebServiceParameters peer_; std::unique_ptr client_; + bool transcode_; + DicomTransferSyntax transferSyntax_; protected: virtual bool HandleInstance(const std::string& instance); @@ -56,7 +58,8 @@ public: OrthancPeerStoreJob(ServerContext& context) : - context_(context) + context_(context), + transcode_(false) { } @@ -70,6 +73,19 @@ return peer_; } + bool IsTranscode() const + { + return transcode_; + } + + DicomTransferSyntax GetTransferSyntax() const; + + void SetTranscode(DicomTransferSyntax syntax); + + void SetTranscode(const std::string& transferSyntaxUid); + + void ClearTranscode(); + virtual void Stop(JobStopReason reason); // For pausing jobs virtual void GetJobType(std::string& target) diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/ServerJobs/ResourceModificationJob.cpp --- a/OrthancServer/ServerJobs/ResourceModificationJob.cpp Wed May 20 16:38:33 2020 +0200 +++ b/OrthancServer/ServerJobs/ResourceModificationJob.cpp Wed May 20 17:03:24 2020 +0200 @@ -38,6 +38,10 @@ #include "../../Core/SerializationToolbox.h" #include "../ServerContext.h" +#include +#include +#include + namespace Orthanc { class ResourceModificationJob::Output : public boost::noncopyable @@ -152,7 +156,7 @@ try { - ServerContext::DicomCacheLocker locker(context_, instance); + ServerContext::DicomCacheLocker locker(GetContext(), instance); ParsedDicomFile& original = locker.GetDicom(); originalHasher.reset(new DicomInstanceHasher(original.GetHasher())); @@ -171,6 +175,41 @@ modification_->Apply(*modified); + const std::string modifiedUid = IDicomTranscoder::GetSopInstanceUid(modified->GetDcmtkObject()); + + if (transcode_) + { + std::set syntaxes; + syntaxes.insert(transferSyntax_); + + IDicomTranscoder::DicomImage source; + source.AcquireParsed(*modified); // "modified" is invalid below this point + + IDicomTranscoder::DicomImage transcoded; + if (GetContext().Transcode(transcoded, source, syntaxes, true)) + { + modified.reset(transcoded.ReleaseAsParsedDicomFile()); + + // Fix the SOP instance UID in order the preserve the + // references between instance UIDs in the DICOM hierarchy + // (the UID might have changed in the case of lossy transcoding) + if (modified.get() == NULL || + modified->GetDcmtkObject().getDataset() == NULL || + !modified->GetDcmtkObject().getDataset()->putAndInsertString( + DCM_SOPInstanceUID, modifiedUid.c_str(), OFTrue /* replace */).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + } + else + { + LOG(WARNING) << "Cannot transcode instance, keeping original transfer syntax: " << instance; + modified.reset(source.ReleaseAsParsedDicomFile()); + } + } + + assert(modifiedUid == IDicomTranscoder::GetSopInstanceUid(modified->GetDcmtkObject())); + DicomInstanceToStore toStore; toStore.SetOrigin(origin_); toStore.SetParsedDicomFile(*modified); @@ -211,24 +250,32 @@ **/ std::string modifiedInstance; - if (context_.Store(modifiedInstance, toStore, - StoreInstanceMode_Default) != StoreStatus_Success) + if (GetContext().Store(modifiedInstance, toStore, + StoreInstanceMode_Default) != StoreStatus_Success) { throw OrthancException(ErrorCode_CannotStoreInstance, "Error while storing a modified instance " + instance); } - assert(modifiedInstance == modifiedHasher.HashInstance()); + /** + * The assertion below will fail if automated transcoding to a + * lossy transfer syntax is enabled in the Orthanc core, and if + * the source instance is not in this transfer syntax. + **/ + // assert(modifiedInstance == modifiedHasher.HashInstance()); output_->Update(modifiedHasher); return true; } - - bool ResourceModificationJob::HandleTrailingStep() + + ResourceModificationJob::ResourceModificationJob(ServerContext& context) : + CleaningInstancesJob(context, true /* by default, keep source */), + modification_(new DicomModification), + isAnonymization_(false), + transcode_(false) { - throw OrthancException(ErrorCode_InternalError); } @@ -285,9 +332,64 @@ } + DicomTransferSyntax ResourceModificationJob::GetTransferSyntax() const + { + if (transcode_) + { + return transferSyntax_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void ResourceModificationJob::SetTranscode(DicomTransferSyntax syntax) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + transcode_ = true; + transferSyntax_ = syntax; + } + } + + + void ResourceModificationJob::SetTranscode(const std::string& transferSyntaxUid) + { + DicomTransferSyntax s; + if (LookupTransferSyntax(s, transferSyntaxUid)) + { + SetTranscode(s); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, + "Unknown transfer syntax UID: " + transferSyntaxUid); + } + } + + + void ResourceModificationJob::ClearTranscode() + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + transcode_ = false; + } + } + + void ResourceModificationJob::GetPublicContent(Json::Value& value) { - SetOfInstancesJob::GetPublicContent(value); + CleaningInstancesJob::GetPublicContent(value); value["IsAnonymization"] = isAnonymization_; @@ -295,33 +397,57 @@ { output_->Format(value); } + + if (transcode_) + { + value["Transcode"] = GetTransferSyntaxUid(transferSyntax_); + } } static const char* MODIFICATION = "Modification"; static const char* ORIGIN = "Origin"; static const char* IS_ANONYMIZATION = "IsAnonymization"; + static const char* TRANSCODE = "Transcode"; ResourceModificationJob::ResourceModificationJob(ServerContext& context, const Json::Value& serialized) : - SetOfInstancesJob(serialized), - context_(context) + CleaningInstancesJob(context, serialized, true /* by default, keep source */) { + assert(serialized.type() == Json::objectValue); + isAnonymization_ = SerializationToolbox::ReadBoolean(serialized, IS_ANONYMIZATION); origin_ = DicomInstanceOrigin(serialized[ORIGIN]); modification_.reset(new DicomModification(serialized[MODIFICATION])); + + if (serialized.isMember(TRANSCODE)) + { + SetTranscode(SerializationToolbox::ReadString(serialized, TRANSCODE)); + } + else + { + transcode_ = false; + } } bool ResourceModificationJob::Serialize(Json::Value& value) { - if (!SetOfInstancesJob::Serialize(value)) + if (!CleaningInstancesJob::Serialize(value)) { return false; } else { + assert(value.type() == Json::objectValue); + value[IS_ANONYMIZATION] = isAnonymization_; + + if (transcode_) + { + value[TRANSCODE] = GetTransferSyntaxUid(transferSyntax_); + } + origin_.Serialize(value[ORIGIN]); Json::Value tmp; diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/ServerJobs/ResourceModificationJob.h --- a/OrthancServer/ServerJobs/ResourceModificationJob.h Wed May 20 16:38:33 2020 +0200 +++ b/OrthancServer/ServerJobs/ResourceModificationJob.h Wed May 20 17:03:24 2020 +0200 @@ -33,36 +33,31 @@ #pragma once -#include "../../Core/JobsEngine/SetOfInstancesJob.h" #include "../../Core/DicomParsing/DicomModification.h" #include "../DicomInstanceOrigin.h" +#include "CleaningInstancesJob.h" namespace Orthanc { class ServerContext; - class ResourceModificationJob : public SetOfInstancesJob + class ResourceModificationJob : public CleaningInstancesJob { private: class Output; - ServerContext& context_; std::unique_ptr modification_; boost::shared_ptr output_; bool isAnonymization_; DicomInstanceOrigin origin_; + bool transcode_; + DicomTransferSyntax transferSyntax_; protected: virtual bool HandleInstance(const std::string& instance); - virtual bool HandleTrailingStep(); - public: - ResourceModificationJob(ServerContext& context) : - context_(context), - isAnonymization_(false) - { - } + ResourceModificationJob(ServerContext& context); ResourceModificationJob(ServerContext& context, const Json::Value& serialized); @@ -87,6 +82,19 @@ return origin_; } + bool IsTranscode() const + { + return transcode_; + } + + DicomTransferSyntax GetTransferSyntax() const; + + void SetTranscode(DicomTransferSyntax syntax); + + void SetTranscode(const std::string& transferSyntaxUid); + + void ClearTranscode(); + virtual void Stop(JobStopReason reason) { } diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/ServerJobs/SplitStudyJob.cpp --- a/OrthancServer/ServerJobs/SplitStudyJob.cpp Wed May 20 16:38:33 2020 +0200 +++ b/OrthancServer/ServerJobs/SplitStudyJob.cpp Wed May 20 17:03:24 2020 +0200 @@ -67,6 +67,12 @@ bool SplitStudyJob::HandleInstance(const std::string& instance) { + if (!HasTrailingStep()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "AddTrailingStep() should have been called after AddSourceSeries()"); + } + /** * Retrieve the DICOM instance to be modified **/ @@ -75,7 +81,7 @@ try { - ServerContext::DicomCacheLocker locker(context_, instance); + ServerContext::DicomCacheLocker locker(GetContext(), instance); modified.reset(locker.GetDicom().Clone(true)); } catch (OrthancException&) @@ -138,8 +144,8 @@ toStore.SetParsedDicomFile(*modified); std::string modifiedInstance; - if (context_.Store(modifiedInstance, toStore, - StoreInstanceMode_Default) != StoreStatus_Success) + if (GetContext().Store(modifiedInstance, toStore, + StoreInstanceMode_Default) != StoreStatus_Success) { LOG(ERROR) << "Error while storing a modified instance " << instance; return false; @@ -149,27 +155,9 @@ } - bool SplitStudyJob::HandleTrailingStep() - { - if (!keepSource_) - { - const size_t n = GetInstancesCount(); - - for (size_t i = 0; i < n; i++) - { - Json::Value tmp; - context_.DeleteResource(tmp, GetInstance(i), ResourceType_Instance); - } - } - - return true; - } - - SplitStudyJob::SplitStudyJob(ServerContext& context, const std::string& sourceStudy) : - context_(context), - keepSource_(false), + CleaningInstancesJob(context, false /* by default, remove source instances */), sourceStudy_(sourceStudy), targetStudyUid_(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study)) { @@ -177,7 +165,7 @@ ResourceType type; - if (!context_.GetIndex().LookupResourceType(type, sourceStudy) || + if (!GetContext().GetIndex().LookupResourceType(type, sourceStudy) || type != ResourceType_Study) { throw OrthancException(ErrorCode_UnknownResource, @@ -213,7 +201,7 @@ { throw OrthancException(ErrorCode_BadSequenceOfCalls); } - else if (!context_.GetIndex().LookupParent(parent, series, ResourceType_Study) || + else if (!GetContext().GetIndex().LookupParent(parent, series, ResourceType_Study) || parent != sourceStudy_) { throw OrthancException(ErrorCode_UnknownResource, @@ -226,7 +214,7 @@ // Add all the instances of the series as to be processed std::list instances; - context_.GetIndex().GetChildren(instances, series); + GetContext().GetIndex().GetChildren(instances, series); for (std::list::const_iterator it = instances.begin(); it != instances.end(); ++it) @@ -237,17 +225,6 @@ } - void SplitStudyJob::SetKeepSource(bool keep) - { - if (IsStarted()) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - - keepSource_ = keep; - } - - bool SplitStudyJob::LookupTargetSeriesUid(std::string& uid, const std::string& series) const { @@ -309,7 +286,7 @@ void SplitStudyJob::GetPublicContent(Json::Value& value) { - SetOfInstancesJob::GetPublicContent(value); + CleaningInstancesJob::GetPublicContent(value); if (!targetStudy_.empty()) { @@ -320,7 +297,6 @@ } - static const char* KEEP_SOURCE = "KeepSource"; static const char* SOURCE_STUDY = "SourceStudy"; static const char* TARGET_STUDY = "TargetStudy"; static const char* TARGET_STUDY_UID = "TargetStudyUID"; @@ -332,8 +308,8 @@ SplitStudyJob::SplitStudyJob(ServerContext& context, const Json::Value& serialized) : - SetOfInstancesJob(serialized), // (*) - context_(context) + CleaningInstancesJob(context, serialized, + false /* by default, remove source instances */) // (*) { if (!HasTrailingStep()) { @@ -343,7 +319,6 @@ Setup(); - keepSource_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOURCE); sourceStudy_ = SerializationToolbox::ReadString(serialized, SOURCE_STUDY); targetStudy_ = SerializationToolbox::ReadString(serialized, TARGET_STUDY); targetStudyUid_ = SerializationToolbox::ReadString(serialized, TARGET_STUDY_UID); @@ -356,13 +331,12 @@ bool SplitStudyJob::Serialize(Json::Value& target) { - if (!SetOfInstancesJob::Serialize(target)) + if (!CleaningInstancesJob::Serialize(target)) { return false; } else { - target[KEEP_SOURCE] = keepSource_; target[SOURCE_STUDY] = sourceStudy_; target[TARGET_STUDY] = targetStudy_; target[TARGET_STUDY_UID] = targetStudyUid_; diff -r 66879215cbf3 -r 596912ebab5f OrthancServer/ServerJobs/SplitStudyJob.h --- a/OrthancServer/ServerJobs/SplitStudyJob.h Wed May 20 16:38:33 2020 +0200 +++ b/OrthancServer/ServerJobs/SplitStudyJob.h Wed May 20 17:03:24 2020 +0200 @@ -33,24 +33,22 @@ #pragma once -#include "../../Core/JobsEngine/SetOfInstancesJob.h" #include "../../Core/DicomFormat/DicomTag.h" #include "../DicomInstanceOrigin.h" +#include "CleaningInstancesJob.h" namespace Orthanc { class ServerContext; - class SplitStudyJob : public SetOfInstancesJob + class SplitStudyJob : public CleaningInstancesJob { private: typedef std::map SeriesUidMap; typedef std::map Replacements; - ServerContext& context_; std::set allowedTags_; - bool keepSource_; std::string sourceStudy_; std::string targetStudy_; std::string targetStudyUid_; @@ -66,8 +64,6 @@ protected: virtual bool HandleInstance(const std::string& instance); - virtual bool HandleTrailingStep(); - public: SplitStudyJob(ServerContext& context, const std::string& sourceStudy); @@ -92,13 +88,6 @@ void AddSourceSeries(const std::string& series); - bool IsKeepSource() const - { - return keepSource_; - } - - void SetKeepSource(bool keep); - bool LookupTargetSeriesUid(std::string& uid, const std::string& series) const; diff -r 66879215cbf3 -r 596912ebab5f Plugins/Engine/OrthancPlugins.cpp --- a/Plugins/Engine/OrthancPlugins.cpp Wed May 20 16:38:33 2020 +0200 +++ b/Plugins/Engine/OrthancPlugins.cpp Wed May 20 17:03:24 2020 +0200 @@ -62,7 +62,6 @@ #include "../../Core/OrthancException.h" #include "../../Core/SerializationToolbox.h" #include "../../Core/Toolbox.h" -#include "../../OrthancServer/DefaultDicomImageDecoder.h" #include "../../OrthancServer/OrthancConfiguration.h" #include "../../OrthancServer/OrthancFindRequestHandler.h" #include "../../OrthancServer/Search/HierarchicalMatcher.h" @@ -71,12 +70,13 @@ #include "PluginsEnumerations.h" #include "PluginsJob.h" -#include +#include #include #include #define ERROR_MESSAGE_64BIT "A 64bit version of the Orthanc API is necessary" + namespace Orthanc { static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, @@ -330,9 +330,11 @@ class DicomWebBinaryFormatter : public DicomWebJsonVisitor::IBinaryFormatter { private: - OrthancPluginDicomWebBinaryCallback callback_; - DicomWebJsonVisitor::BinaryMode currentMode_; - std::string currentBulkDataUri_; + OrthancPluginDicomWebBinaryCallback oldCallback_; + OrthancPluginDicomWebBinaryCallback2 newCallback_; // New in Orthanc 1.7.0 + void* newPayload_; // New in Orthanc 1.7.0 + DicomWebJsonVisitor::BinaryMode currentMode_; + std::string currentBulkDataUri_; static void Setter(OrthancPluginDicomWebNode* node, OrthancPluginDicomWebBinaryMode mode, @@ -366,8 +368,18 @@ } public: - DicomWebBinaryFormatter(const _OrthancPluginEncodeDicomWeb& parameters) : - callback_(parameters.callback) + DicomWebBinaryFormatter(OrthancPluginDicomWebBinaryCallback callback) : + oldCallback_(callback), + newCallback_(NULL), + newPayload_(NULL) + { + } + + DicomWebBinaryFormatter(OrthancPluginDicomWebBinaryCallback2 callback, + void* payload) : + oldCallback_(NULL), + newCallback_(callback), + newPayload_(payload) { } @@ -377,7 +389,8 @@ const DicomTag& tag, ValueRepresentation vr) { - if (callback_ == NULL) + if (oldCallback_ == NULL && + newCallback_ == NULL) { return DicomWebJsonVisitor::BinaryMode_InlineBinary; } @@ -398,20 +411,70 @@ currentMode_ = DicomWebJsonVisitor::BinaryMode_Ignore; - callback_(reinterpret_cast(this), - DicomWebBinaryFormatter::Setter, - static_cast(parentTags.size()), - (empty ? NULL : &groups[0]), - (empty ? NULL : &elements[0]), - (empty ? NULL : &indexes[0]), - tag.GetGroup(), - tag.GetElement(), - Plugins::Convert(vr)); + if (oldCallback_ != NULL) + { + oldCallback_(reinterpret_cast(this), + DicomWebBinaryFormatter::Setter, + static_cast(parentTags.size()), + (empty ? NULL : &groups[0]), + (empty ? NULL : &elements[0]), + (empty ? NULL : &indexes[0]), + tag.GetGroup(), + tag.GetElement(), + Plugins::Convert(vr)); + } + else + { + assert(newCallback_ != NULL); + newCallback_(reinterpret_cast(this), + DicomWebBinaryFormatter::Setter, + static_cast(parentTags.size()), + (empty ? NULL : &groups[0]), + (empty ? NULL : &elements[0]), + (empty ? NULL : &indexes[0]), + tag.GetGroup(), + tag.GetElement(), + Plugins::Convert(vr), + newPayload_); + } bulkDataUri = currentBulkDataUri_; return currentMode_; } } + + void Apply(char** target, + bool isJson, + ParsedDicomFile& dicom) + { + DicomWebJsonVisitor visitor; + visitor.SetFormatter(*this); + + dicom.Apply(visitor); + + std::string s; + + if (isJson) + { + s = visitor.GetResult().toStyledString(); + } + else + { + visitor.FormatXml(s); + } + + *target = CopyString(s); + } + + + void Apply(char** target, + bool isJson, + const void* dicom, + size_t dicomSize) + { + ParsedDicomFile parsed(dicom, dicomSize); + Apply(target, isJson, parsed); + } }; } @@ -827,6 +890,7 @@ typedef std::list IncomingHttpRequestFilters2; typedef std::list IncomingDicomInstanceFilters; typedef std::list DecodeImageCallbacks; + typedef std::list TranscoderCallbacks; typedef std::list JobsUnserializers; typedef std::list RefreshMetricsCallbacks; typedef std::list StorageCommitmentScpCallbacks; @@ -841,6 +905,7 @@ OrthancPluginFindCallback findCallback_; OrthancPluginWorklistCallback worklistCallback_; DecodeImageCallbacks decodeImageCallbacks_; + TranscoderCallbacks transcoderCallbacks_; JobsUnserializers jobsUnserializers_; _OrthancPluginMoveCallback moveCallbacks_; IncomingHttpRequestFilters incomingHttpRequestFilters_; @@ -855,7 +920,7 @@ boost::recursive_mutex changeCallbackMutex_; boost::mutex findCallbackMutex_; boost::mutex worklistCallbackMutex_; - boost::mutex decodeImageCallbackMutex_; + boost::shared_mutex decoderTranscoderMutex_; // Changed from "boost::mutex" in Orthanc 1.7.0 boost::mutex jobsUnserializersMutex_; boost::mutex refreshMetricsMutex_; boost::mutex storageCommitmentScpMutex_; @@ -1831,8 +1896,8 @@ DicomInstanceToStore instance_; public: - DicomInstanceFromTranscoded(IDicomTranscoder::TranscodedDicom& transcoded) : - parsed_(ParsedDicomFile::AcquireDcmtkObject(transcoded.ReleaseDicom())) + DicomInstanceFromTranscoded(IDicomTranscoder::DicomImage& transcoded) : + parsed_(transcoded.ReleaseAsParsedDicomFile()) { instance_.SetParsedDicomFile(*parsed_); instance_.SetOrigin(DicomInstanceOrigin::FromPlugins()); @@ -2046,7 +2111,7 @@ const _OrthancPluginDecodeImageCallback& p = *reinterpret_cast(parameters); - boost::mutex::scoped_lock lock(pimpl_->decodeImageCallbackMutex_); + boost::unique_lock lock(pimpl_->decoderTranscoderMutex_); pimpl_->decodeImageCallbacks_.push_back(p.callback); LOG(INFO) << "Plugin has registered a callback to decode DICOM images (" @@ -2054,6 +2119,19 @@ } + void OrthancPlugins::RegisterTranscoderCallback(const void* parameters) + { + const _OrthancPluginTranscoderCallback& p = + *reinterpret_cast(parameters); + + boost::unique_lock lock(pimpl_->decoderTranscoderMutex_); + + pimpl_->transcoderCallbacks_.push_back(p.callback); + LOG(INFO) << "Plugin has registered a callback to transcode DICOM images (" + << pimpl_->transcoderCallbacks_.size() << " transcoder(s) now active)"; + } + + void OrthancPlugins::RegisterJobsUnserializer(const void* parameters) { const _OrthancPluginJobsUnserializer& p = @@ -2693,6 +2771,11 @@ // Images returned to plugins are assumed to be writeable. If the // input image is read-only, we return a copy so that it can be modified. + if (image.get() == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + if (image->IsReadOnly()) { std::unique_ptr copy(new Image(image->GetFormat(), image->GetWidth(), image->GetHeight(), false)); @@ -2746,30 +2829,17 @@ case _OrthancPluginService_GetInstanceDecodedFrame: { - bool hasDecoderPlugin; - - { - boost::mutex::scoped_lock lock(pimpl_->decodeImageCallbackMutex_); - hasDecoderPlugin = !pimpl_->decodeImageCallbacks_.empty(); - } - - std::unique_ptr decoded; if (p.targetImage == NULL) { throw OrthancException(ErrorCode_NullPointer); } - else if (hasDecoderPlugin) + + std::unique_ptr decoded; { - // TODO - This call could be speeded up the future, if a - // "decoding context" gets introduced in the decoder plugins - - decoded.reset(Decode(instance.GetBufferData(), instance.GetBufferSize(), p.frameIndex)); + PImpl::ServerContextLock lock(*pimpl_); + decoded.reset(lock.GetContext().DecodeDicomFrame(instance, p.frameIndex)); } - else - { - decoded.reset(DicomImageDecoder::Decode(instance.GetParsedDicomFile(), p.frameIndex)); - } - + *(p.targetImage) = ReturnImage(decoded); return; } @@ -2789,7 +2859,39 @@ CopyToMemoryBuffer(*p.targetBuffer, serialized); return; } + + case _OrthancPluginService_GetInstanceAdvancedJson: + { + if (p.targetStringToFree == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + Json::Value json; + instance.GetParsedDicomFile().DatasetToJson( + json, Plugins::Convert(p.format), + static_cast(p.flags), p.maxStringLength); + + Json::FastWriter writer; + *p.targetStringToFree = CopyString(writer.write(json)); + return; + } + + case _OrthancPluginService_GetInstanceDicomWebJson: + case _OrthancPluginService_GetInstanceDicomWebXml: + { + if (p.targetStringToFree == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + DicomWebBinaryFormatter formatter(p.dicomWebCallback, p.dicomWebPayload); + formatter.Apply(p.targetStringToFree, + (service == _OrthancPluginService_GetInstanceDicomWebJson), + instance.GetParsedDicomFile()); + return; + } + default: throw OrthancException(ErrorCode_InternalError); } @@ -3675,6 +3777,9 @@ case _OrthancPluginService_GetInstanceRawFrame: case _OrthancPluginService_GetInstanceDecodedFrame: case _OrthancPluginService_SerializeDicomInstance: + case _OrthancPluginService_GetInstanceAdvancedJson: + case _OrthancPluginService_GetInstanceDicomWebJson: + case _OrthancPluginService_GetInstanceDicomWebXml: AccessDicomInstance2(service, parameters); return true; @@ -4195,28 +4300,23 @@ const _OrthancPluginEncodeDicomWeb& p = *reinterpret_cast(parameters); - DicomWebBinaryFormatter formatter(p); - - DicomWebJsonVisitor visitor; - visitor.SetFormatter(formatter); - - { - ParsedDicomFile dicom(p.dicom, p.dicomSize); - dicom.Apply(visitor); - } - - std::string s; - - if (service == _OrthancPluginService_EncodeDicomWebJson) - { - s = visitor.GetResult().toStyledString(); - } - else - { - visitor.FormatXml(s); - } - - *p.target = CopyString(s); + DicomWebBinaryFormatter formatter(p.callback); + formatter.Apply(p.target, + (service == _OrthancPluginService_EncodeDicomWebJson), + p.dicom, p.dicomSize); + return true; + } + + case _OrthancPluginService_EncodeDicomWebJson2: + case _OrthancPluginService_EncodeDicomWebXml2: + { + const _OrthancPluginEncodeDicomWeb2& p = + *reinterpret_cast(parameters); + + DicomWebBinaryFormatter formatter(p.callback, p.payload); + formatter.Apply(p.target, + (service == _OrthancPluginService_EncodeDicomWebJson2), + p.dicom, p.dicomSize); return true; } @@ -4269,32 +4369,52 @@ } else { - ParsedDicomFile dicom(p.buffer, p.size); - std::set syntaxes; syntaxes.insert(transferSyntax); - - std::unique_ptr transcoded; + + IDicomTranscoder::DicomImage source; + source.SetExternalBuffer(p.buffer, p.size); + + IDicomTranscoder::DicomImage transcoded; + bool success; { PImpl::ServerContextLock lock(*pimpl_); - transcoded.reset(lock.GetContext().GetTranscoder().TranscodeToParsed( - dicom.GetDcmtkObject(), p.buffer, p.size, - syntaxes, true /* allow new sop */)); + success = lock.GetContext().Transcode( + transcoded, source, syntaxes, true /* allow new sop */); } - if (transcoded.get() == NULL) + if (success) + { + *(p.target) = reinterpret_cast( + new DicomInstanceFromTranscoded(transcoded)); + return true; + } + else { throw OrthancException(ErrorCode_NotImplemented, "Cannot transcode image"); } - else - { - *(p.target) = reinterpret_cast( - new DicomInstanceFromTranscoded(*transcoded)); - return true; - } } } + + case _OrthancPluginService_CreateMemoryBuffer: + { + const _OrthancPluginCreateMemoryBuffer& p = + *reinterpret_cast(parameters); + + p.target->size = p.size; + + if (p.size == 0) + { + p.target->data = NULL; + } + else + { + p.target->data = malloc(p.size); + } + + return true; + } default: return false; @@ -4349,6 +4469,10 @@ RegisterDecodeImageCallback(parameters); return true; + case _OrthancPluginService_RegisterTranscoderCallback: + RegisterTranscoderCallback(parameters); + return true; + case _OrthancPluginService_RegisterJobsUnserializer: RegisterJobsUnserializer(parameters); return true; @@ -4733,16 +4857,23 @@ bool OrthancPlugins::HasCustomImageDecoder() { - boost::mutex::scoped_lock lock(pimpl_->decodeImageCallbackMutex_); + boost::shared_lock lock(pimpl_->decoderTranscoderMutex_); return !pimpl_->decodeImageCallbacks_.empty(); } - ImageAccessor* OrthancPlugins::DecodeUnsafe(const void* dicom, - size_t size, - unsigned int frame) + bool OrthancPlugins::HasCustomTranscoder() { - boost::mutex::scoped_lock lock(pimpl_->decodeImageCallbackMutex_); + boost::shared_lock lock(pimpl_->decoderTranscoderMutex_); + return !pimpl_->transcoderCallbacks_.empty(); + } + + + ImageAccessor* OrthancPlugins::Decode(const void* dicom, + size_t size, + unsigned int frame) + { + boost::shared_lock lock(pimpl_->decoderTranscoderMutex_); for (PImpl::DecodeImageCallbacks::const_iterator decoder = pimpl_->decodeImageCallbacks_.begin(); @@ -4759,25 +4890,6 @@ return NULL; } - - ImageAccessor* OrthancPlugins::Decode(const void* dicom, - size_t size, - unsigned int frame) - { - ImageAccessor* result = DecodeUnsafe(dicom, size, frame); - - if (result != NULL) - { - return result; - } - else - { - LOG(INFO) << "The installed image decoding plugins cannot handle an image, fallback to the built-in decoder"; - DefaultDicomImageDecoder defaultDecoder; - return defaultDecoder.Decode(dicom, size, frame); - } - } - bool OrthancPlugins::IsAllowed(HttpMethod method, const char* uri, @@ -5064,16 +5176,79 @@ } - bool OrthancPlugins::Transcode(std::string& target, - DicomTransferSyntax& sourceSyntax /* out */, - DicomTransferSyntax& targetSyntax /* out */, - bool& hasSopInstanceUidChanged /* out */, - const void* buffer, - size_t size, - const std::set& allowedSyntaxes, - bool allowNewSopInstanceUid) + class MemoryBufferRaii : public boost::noncopyable { - // TODO + private: + OrthancPluginMemoryBuffer buffer_; + + public: + MemoryBufferRaii() + { + buffer_.size = 0; + buffer_.data = NULL; + } + + ~MemoryBufferRaii() + { + if (buffer_.size != 0) + { + free(buffer_.data); + } + } + + OrthancPluginMemoryBuffer* GetObject() + { + return &buffer_; + } + + void ToString(std::string& target) const + { + target.resize(buffer_.size); + + if (buffer_.size != 0) + { + memcpy(&target[0], buffer_.data, buffer_.size); + } + } + }; + + + bool OrthancPlugins::TranscodeBuffer(std::string& target, + const void* buffer, + size_t size, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) + { + boost::shared_lock lock(pimpl_->decoderTranscoderMutex_); + + if (pimpl_->transcoderCallbacks_.empty()) + { + return NULL; + } + + std::vector uids; + uids.reserve(allowedSyntaxes.size()); + for (std::set::const_iterator + it = allowedSyntaxes.begin(); it != allowedSyntaxes.end(); ++it) + { + uids.push_back(GetTransferSyntaxUid(*it)); + } + + for (PImpl::TranscoderCallbacks::const_iterator + transcoder = pimpl_->transcoderCallbacks_.begin(); + transcoder != pimpl_->transcoderCallbacks_.end(); ++transcoder) + { + MemoryBufferRaii a; + + if ((*transcoder) (a.GetObject(), buffer, size, uids.empty() ? NULL : &uids[0], + static_cast(uids.size()), allowNewSopInstanceUid) == + OrthancPluginErrorCode_Success) + { + a.ToString(target); + return true; + } + } + return false; } } diff -r 66879215cbf3 -r 596912ebab5f Plugins/Engine/OrthancPlugins.h --- a/Plugins/Engine/OrthancPlugins.h Wed May 20 16:38:33 2020 +0200 +++ b/Plugins/Engine/OrthancPlugins.h Wed May 20 17:03:24 2020 +0200 @@ -124,6 +124,8 @@ void RegisterDecodeImageCallback(const void* parameters); + void RegisterTranscoderCallback(const void* parameters); + void RegisterJobsUnserializer(const void* parameters); void RegisterIncomingHttpRequestFilter(const void* parameters); @@ -237,14 +239,11 @@ protected: // From "MemoryBufferTranscoder" - virtual bool Transcode(std::string& target, - DicomTransferSyntax& sourceSyntax /* out */, - DicomTransferSyntax& targetSyntax /* out */, - bool& hasSopInstanceUidChanged /* out */, - const void* buffer, - size_t size, - const std::set& allowedSyntaxes, - bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; + virtual bool TranscodeBuffer(std::string& target, + const void* buffer, + size_t size, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; public: OrthancPlugins(); @@ -328,12 +327,7 @@ bool HasCustomImageDecoder(); - // Contrarily to "Decode()", this method does not fallback to the - // builtin image decoder, if no installed custom decoder can - // handle the image (it returns NULL in this case). - ImageAccessor* DecodeUnsafe(const void* dicom, - size_t size, - unsigned int frame); + bool HasCustomTranscoder(); virtual ImageAccessor* Decode(const void* dicom, size_t size, diff -r 66879215cbf3 -r 596912ebab5f Plugins/Include/orthanc/OrthancCPlugin.h --- a/Plugins/Include/orthanc/OrthancCPlugin.h Wed May 20 16:38:33 2020 +0200 +++ b/Plugins/Include/orthanc/OrthancCPlugin.h Wed May 20 17:03:24 2020 +0200 @@ -28,6 +28,7 @@ * - Possibly register a callback to answer chunked HTTP transfers using ::OrthancPluginRegisterChunkedRestCallback(). * - Possibly register a callback for Storage Commitment SCP using ::OrthancPluginRegisterStorageCommitmentScpCallback(). * - Possibly register a callback to filter incoming DICOM instance using OrthancPluginRegisterIncomingDicomInstanceFilter(). + * - Possibly register a custom transcoder for DICOM images using OrthancPluginRegisterTranscoderCallback(). * -# void OrthancPluginFinalize(): * This function is invoked by Orthanc during its shutdown. The plugin * must free all its memory. @@ -64,6 +65,9 @@ * * @defgroup Orthanc Orthanc * @brief Functions to access the content of the Orthanc server. + * + * @defgroup DicomInstance DicomInstance + * @brief Functions to access DICOM images that are managed by the Orthanc core. **/ @@ -438,8 +442,11 @@ _OrthancPluginService_SetMetricsValue = 31, _OrthancPluginService_EncodeDicomWebJson = 32, _OrthancPluginService_EncodeDicomWebXml = 33, - _OrthancPluginService_ChunkedHttpClient = 34, /* New in Orthanc 1.5.7 */ - _OrthancPluginService_GetTagName = 35, /* New in Orthanc 1.5.7 */ + _OrthancPluginService_ChunkedHttpClient = 34, /* New in Orthanc 1.5.7 */ + _OrthancPluginService_GetTagName = 35, /* New in Orthanc 1.5.7 */ + _OrthancPluginService_EncodeDicomWebJson2 = 36, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_EncodeDicomWebXml2 = 37, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_CreateMemoryBuffer = 38, /* New in Orthanc 1.7.0 */ /* Registration of callbacks */ _OrthancPluginService_RegisterRestCallback = 1000, @@ -457,7 +464,8 @@ _OrthancPluginService_RegisterChunkedRestCallback = 1012, /* New in Orthanc 1.5.7 */ _OrthancPluginService_RegisterStorageCommitmentScpCallback = 1013, _OrthancPluginService_RegisterIncomingDicomInstanceFilter = 1014, - + _OrthancPluginService_RegisterTranscoderCallback = 1015, /* New in Orthanc 1.7.0 */ + /* Sending answers to REST calls */ _OrthancPluginService_AnswerBuffer = 2000, _OrthancPluginService_CompressAndAnswerPngImage = 2001, /* Unused as of Orthanc 0.9.4 */ @@ -510,6 +518,9 @@ _OrthancPluginService_GetInstanceDecodedFrame = 4014, /* New in Orthanc 1.7.0 */ _OrthancPluginService_TranscodeDicomInstance = 4015, /* New in Orthanc 1.7.0 */ _OrthancPluginService_SerializeDicomInstance = 4016, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GetInstanceAdvancedJson = 4017, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GetInstanceDicomWebJson = 4018, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GetInstanceDicomWebXml = 4019, /* New in Orthanc 1.7.0 */ /* Services for plugins implementing a database back-end */ _OrthancPluginService_RegisterDatabaseBackend = 5000, @@ -1017,7 +1028,8 @@ /** - * @brief Opaque structure that represents a DICOM instance received by Orthanc. + * @brief Opaque structure that represents a DICOM instance that is managed by the Orthanc core. + * @ingroup DicomInstance **/ typedef struct _OrthancPluginDicomInstance_t OrthancPluginDicomInstance; @@ -1617,6 +1629,45 @@ /** + * @brief Callback executed to encode a binary tag in DICOMweb. + * + * Signature of a callback function that is called by Orthanc + * whenever a DICOM tag that contains a binary value must be written + * to a JSON or XML node, while a DICOMweb document is being + * generated. The value representation (VR) of the DICOM tag can be + * OB, OD, OF, OL, OW, or UN. + * + * @see OrthancPluginEncodeDicomWebJson() and OrthancPluginEncodeDicomWebXml() + * @param node The node being generated, as provided by Orthanc. + * @param setter The setter to be used to encode the content of the node. If + * the setter is not called, the binary tag is not written to the output document. + * @param levelDepth The depth of the node in the DICOM hierarchy of sequences. + * This parameter gives the number of elements in the "levelTagGroup", + * "levelTagElement", and "levelIndex" arrays. + * @param levelTagGroup The group of the parent DICOM tags in the hierarchy. + * @param levelTagElement The element of the parent DICOM tags in the hierarchy. + * @param levelIndex The index of the node in the parent sequences of the hierarchy. + * @param tagGroup The group of the DICOM tag of interest. + * @param tagElement The element of the DICOM tag of interest. + * @param vr The value representation of the binary DICOM node. + * @param payload The user payload. + * @ingroup Callbacks + **/ + typedef void (*OrthancPluginDicomWebBinaryCallback2) ( + OrthancPluginDicomWebNode* node, + OrthancPluginDicomWebSetBinaryNode setter, + uint32_t levelDepth, + const uint16_t* levelTagGroup, + const uint16_t* levelTagElement, + const uint32_t* levelIndex, + uint16_t tagGroup, + uint16_t tagElement, + OrthancPluginValueRepresentation vr, + void* payload); + + + + /** * @brief Data structure that contains information about the Orthanc core. **/ typedef struct _OrthancPluginContext_t @@ -2723,7 +2774,7 @@ * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @param instance The instance of interest. * @return The AET if success, NULL if error. - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceRemoteAet( OrthancPluginContext* context, @@ -2756,7 +2807,7 @@ * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @param instance The instance of interest. * @return The size of the file, -1 in case of error. - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE int64_t OrthancPluginGetInstanceSize( OrthancPluginContext* context, @@ -2789,7 +2840,7 @@ * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @param instance The instance of interest. * @return The pointer to the DICOM data, NULL in case of error. - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE const void* OrthancPluginGetInstanceData( OrthancPluginContext* context, @@ -2825,7 +2876,7 @@ * @param instance The instance of interest. * @return The NULL value in case of error, or a string containing the JSON file. * This string must be freed by OrthancPluginFreeString(). - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceJson( OrthancPluginContext* context, @@ -2863,7 +2914,7 @@ * @param instance The instance of interest. * @return The NULL value in case of error, or a string containing the JSON file. * This string must be freed by OrthancPluginFreeString(). - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceSimplifiedJson( OrthancPluginContext* context, @@ -2902,7 +2953,7 @@ * @param instance The instance of interest. * @param metadata The metadata of interest. * @return 1 if the metadata is present, 0 if it is absent, -1 in case of error. - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE int OrthancPluginHasInstanceMetadata( OrthancPluginContext* context, @@ -2943,7 +2994,7 @@ * returned string belongs to the instance object and must NOT be * deallocated. Please make a copy of the string if you wish to access * it later. - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceMetadata( OrthancPluginContext* context, @@ -5115,7 +5166,7 @@ * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @param instance The instance of interest. * @return The origin of the instance. - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE OrthancPluginInstanceOrigin OrthancPluginGetInstanceOrigin( OrthancPluginContext* context, @@ -5191,8 +5242,11 @@ /** * @brief Register a callback to handle the decoding of DICOM images. * - * This function registers a custom callback to the decoding of - * DICOM images, replacing the built-in decoder of Orthanc. + * This function registers a custom callback to decode DICOM images, + * extending the built-in decoder of Orthanc that uses + * DCMTK. Starting with Orthanc 1.7.0, the exact behavior is + * affected by the configuration option + * "BuiltinDecoderTranscoderOrder" of Orthanc. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @param callback The callback. @@ -5324,6 +5378,7 @@ * @param frameIndex The index of the frame of interest in a multi-frame image. * @return The uncompressed image. It must be freed with OrthancPluginFreeImage(). * @ingroup Images + * @see OrthancPluginGetInstanceDecodedFrame() **/ ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginDecodeDicomImage( OrthancPluginContext* context, @@ -6799,6 +6854,7 @@ * @see OrthancPluginCreateDicom() * @return The NULL value in case of error, or the JSON document. This string must * be freed by OrthancPluginFreeString(). + * @deprecated OrthancPluginEncodeDicomWebJson2() * @ingroup Toolbox **/ ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson( @@ -6837,9 +6893,10 @@ * @param dicom Pointer to the DICOM instance. * @param dicomSize Size of the DICOM instance. * @param callback Callback to set the value of the binary tags. - * @return The NULL value in case of error, or the JSON document. This string must + * @return The NULL value in case of error, or the XML document. This string must * be freed by OrthancPluginFreeString(). * @see OrthancPluginCreateDicom() + * @deprecated OrthancPluginEncodeDicomWebXml2() * @ingroup Toolbox **/ ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml( @@ -6869,6 +6926,104 @@ + typedef struct + { + char** target; + const void* dicom; + uint32_t dicomSize; + OrthancPluginDicomWebBinaryCallback2 callback; + void* payload; + } _OrthancPluginEncodeDicomWeb2; + + /** + * @brief Convert a DICOM instance to DICOMweb JSON. + * + * This function converts a memory buffer containing a DICOM instance, + * into its DICOMweb JSON representation. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param dicom Pointer to the DICOM instance. + * @param dicomSize Size of the DICOM instance. + * @param callback Callback to set the value of the binary tags. + * @param payload User payload. + * @return The NULL value in case of error, or the JSON document. This string must + * be freed by OrthancPluginFreeString(). + * @see OrthancPluginCreateDicom() + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson2( + OrthancPluginContext* context, + const void* dicom, + uint32_t dicomSize, + OrthancPluginDicomWebBinaryCallback2 callback, + void* payload) + { + char* target = NULL; + + _OrthancPluginEncodeDicomWeb2 params; + params.target = ⌖ + params.dicom = dicom; + params.dicomSize = dicomSize; + params.callback = callback; + params.payload = payload; + + if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebJson2, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + /** + * @brief Convert a DICOM instance to DICOMweb XML. + * + * This function converts a memory buffer containing a DICOM instance, + * into its DICOMweb XML representation. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param dicom Pointer to the DICOM instance. + * @param dicomSize Size of the DICOM instance. + * @param callback Callback to set the value of the binary tags. + * @param payload User payload. + * @return The NULL value in case of error, or the XML document. This string must + * be freed by OrthancPluginFreeString(). + * @see OrthancPluginCreateDicom() + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml2( + OrthancPluginContext* context, + const void* dicom, + uint32_t dicomSize, + OrthancPluginDicomWebBinaryCallback2 callback, + void* payload) + { + char* target = NULL; + + _OrthancPluginEncodeDicomWeb2 params; + params.target = ⌖ + params.dicom = dicom; + params.dicomSize = dicomSize; + params.callback = callback; + params.payload = payload; + + if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebXml2, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + /** * @brief Callback executed when a HTTP header is received during a chunked transfer. * @@ -7484,7 +7639,7 @@ * @param instance The instance of interest. * @return The NULL value in case of error, or a string containing the * transfer syntax UID. This string must be freed by OrthancPluginFreeString(). - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceTransferSyntaxUid( OrthancPluginContext* context, @@ -7519,7 +7674,7 @@ * @param instance The instance of interest. * @return "1" if the DICOM instance contains pixel data, or "0" if * the tag is missing, or "-1" in the case of an error. - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE int32_t OrthancPluginHasInstancePixelData( OrthancPluginContext* context, @@ -7558,6 +7713,19 @@ const char* transferSyntax; } _OrthancPluginCreateDicomInstance; + /** + * @brief Parse a DICOM instance. + * + * This function parses a memory buffer that contains a DICOM + * file. The function returns a new pointer to a data structure that + * is managed by the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param buffer The memory buffer containing the DICOM instance. + * @param size The size of the memory buffer. + * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance(). + * @ingroup DicomInstance + **/ ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginCreateDicomInstance( OrthancPluginContext* context, const void* buffer, @@ -7586,6 +7754,16 @@ OrthancPluginDicomInstance* dicom; } _OrthancPluginFreeDicomInstance; + /** + * @brief Free a DICOM instance. + * + * This function frees a DICOM instance that was parsed using + * OrthancPluginCreateDicomInstance(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param dicom The DICOM instance. + * @ingroup DicomInstance + **/ ORTHANC_PLUGIN_INLINE void OrthancPluginFreeDicomInstance( OrthancPluginContext* context, OrthancPluginDicomInstance* dicom) @@ -7599,13 +7777,30 @@ typedef struct { - uint32_t* targetUint32; - OrthancPluginMemoryBuffer* targetBuffer; - OrthancPluginImage** targetImage; - const OrthancPluginDicomInstance* instance; - uint32_t frameIndex; + uint32_t* targetUint32; + OrthancPluginMemoryBuffer* targetBuffer; + OrthancPluginImage** targetImage; + char** targetStringToFree; + const OrthancPluginDicomInstance* instance; + uint32_t frameIndex; + OrthancPluginDicomToJsonFormat format; + OrthancPluginDicomToJsonFlags flags; + uint32_t maxStringLength; + OrthancPluginDicomWebBinaryCallback2 dicomWebCallback; + void* dicomWebPayload; } _OrthancPluginAccessDicomInstance2; + /** + * @brief Get the number of frames in a DICOM instance. + * + * This function returns the number of frames that are part of a + * DICOM image managed by the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return The number of frames (will be zero in the case of an error). + * @ingroup DicomInstance + **/ ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetInstanceFramesCount( OrthancPluginContext* context, const OrthancPluginDicomInstance* instance) @@ -7628,6 +7823,24 @@ } } + + /** + * @brief Get the raw content of a frame in a DICOM instance. + * + * This function returns a memory buffer containing the raw content + * of a frame in a DICOM instance that is managed by the Orthanc + * core. This is notably useful for compressed transfer syntaxes, as + * it gives access to the embedded files (such as JPEG, JPEG-LS or + * JPEG2k). The Orthanc core transparently reassembles the fragments + * to extract the raw frame. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param instance The instance of interest. + * @param frameIndex The index of the frame of interest. + * @return 0 if success, or the error code if failure. + * @ingroup DicomInstance + **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetInstanceRawFrame( OrthancPluginContext* context, OrthancPluginMemoryBuffer* target, @@ -7643,6 +7856,19 @@ return context->InvokeService(context, _OrthancPluginService_GetInstanceRawFrame, ¶ms); } + + /** + * @brief Decode one frame from a DICOM instance. + * + * This function decodes one frame of a DICOM image that is managed + * by the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @param frameIndex The index of the frame of interest. + * @return The uncompressed image. It must be freed with OrthancPluginFreeImage(). + * @ingroup DicomInstance + **/ ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginGetInstanceDecodedFrame( OrthancPluginContext* context, const OrthancPluginDicomInstance* instance, @@ -7666,6 +7892,22 @@ } } + + /** + * @brief Parse and transcode a DICOM instance. + * + * This function parses a memory buffer that contains a DICOM file, + * then transcodes it to the given transfer syntax. The function + * returns a new pointer to a data structure that is managed by the + * Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param buffer The memory buffer containing the DICOM instance. + * @param size The size of the memory buffer. + * @param transferSyntax The transfer syntax UID for the transcoding. + * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance(). + * @ingroup DicomInstance + **/ ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginTranscodeDicomInstance( OrthancPluginContext* context, const void* buffer, @@ -7691,6 +7933,19 @@ } } + /** + * @brief Writes a DICOM instance to a memory buffer. + * + * This function returns a memory buffer containing the + * serialization of a DICOM instance that is managed by the Orthanc + * core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param instance The instance of interest. + * @return 0 if success, or the error code if failure. + * @ingroup DicomInstance + **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSerializeDicomInstance( OrthancPluginContext* context, OrthancPluginMemoryBuffer* target, @@ -7703,6 +7958,228 @@ return context->InvokeService(context, _OrthancPluginService_SerializeDicomInstance, ¶ms); } + + + /** + * @brief Format a DICOM memory buffer as a JSON string. + * + * This function takes as DICOM instance managed by the Orthanc + * core, and outputs a JSON string representing the tags of this + * DICOM file. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The DICOM instance of interest. + * @param format The output format. + * @param flags Flags governing the output. + * @param maxStringLength The maximum length of a field. Too long fields will + * be output as "null". The 0 value means no maximum length. + * @return The NULL value if the case of an error, or the JSON + * string. This string must be freed by OrthancPluginFreeString(). + * @ingroup DicomInstance + * @see OrthancPluginDicomBufferToJson + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceAdvancedJson( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance, + OrthancPluginDicomToJsonFormat format, + OrthancPluginDicomToJsonFlags flags, + uint32_t maxStringLength) + { + char* result = NULL; + + _OrthancPluginAccessDicomInstance2 params; + memset(¶ms, 0, sizeof(params)); + params.targetStringToFree = &result; + params.instance = instance; + params.format = format; + params.flags = flags; + params.maxStringLength = maxStringLength; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceAdvancedJson, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Convert a DICOM instance to DICOMweb JSON. + * + * This function converts a DICOM instance that is managed by the + * Orthanc core, into its DICOMweb JSON representation. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The DICOM instance of interest. + * @param callback Callback to set the value of the binary tags. + * @param payload User payload. + * @return The NULL value in case of error, or the JSON document. This string must + * be freed by OrthancPluginFreeString(). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceDicomWebJson( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance, + OrthancPluginDicomWebBinaryCallback2 callback, + void* payload) + { + char* target = NULL; + + _OrthancPluginAccessDicomInstance2 params; + params.targetStringToFree = ⌖ + params.instance = instance; + params.dicomWebCallback = callback; + params.dicomWebPayload = payload; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceDicomWebJson, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + /** + * @brief Convert a DICOM instance to DICOMweb XML. + * + * This function converts a DICOM instance that is managed by the + * Orthanc core, into its DICOMweb XML representation. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The DICOM instance of interest. + * @param callback Callback to set the value of the binary tags. + * @param payload User payload. + * @return The NULL value in case of error, or the XML document. This string must + * be freed by OrthancPluginFreeString(). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceDicomWebXml( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance, + OrthancPluginDicomWebBinaryCallback2 callback, + void* payload) + { + char* target = NULL; + + _OrthancPluginAccessDicomInstance2 params; + params.targetStringToFree = ⌖ + params.instance = instance; + params.dicomWebCallback = callback; + params.dicomWebPayload = payload; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceDicomWebXml, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + + /** + * @brief Signature of a callback function to transcode a DICOM instance. + * @param transcoded Target memory buffer. It must be allocated by the + * plugin using OrthancPluginCreateMemoryBuffer(). + * @param buffer Memory buffer containing the source DICOM instance. + * @param size Size of the source memory buffer. + * @param allowedSyntaxes A C array of possible transfer syntaxes UIDs for the + * result of the transcoding. The plugin must choose by itself the + * transfer syntax that will be used for the resulting DICOM image. + * @param countSyntaxes The number of transfer syntaxes that are contained + * in the "allowedSyntaxes" array. + * @param allowNewSopInstanceUid Whether the transcoding plugin can select + * a transfer syntax that will change the SOP instance UID (or, in other + * terms, whether the plugin can transcode using lossy compression). + * @return 0 if success (i.e. image successfully transcoded and stored into + * "transcoded"), or the error code if failure. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginTranscoderCallback) ( + OrthancPluginMemoryBuffer* transcoded /* out */, + const void* buffer, + uint64_t size, + const char* const* allowedSyntaxes, + uint32_t countSyntaxes, + uint8_t allowNewSopInstanceUid); + + + typedef struct + { + OrthancPluginTranscoderCallback callback; + } _OrthancPluginTranscoderCallback; + + /** + * @brief Register a callback to handle the transcoding of DICOM images. + * + * This function registers a custom callback to transcode DICOM + * images, extending the built-in transcoder of Orthanc that uses + * DCMTK. The exact behavior is affected by the configuration option + * "BuiltinDecoderTranscoderOrder" of Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterTranscoderCallback( + OrthancPluginContext* context, + OrthancPluginTranscoderCallback callback) + { + _OrthancPluginTranscoderCallback params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterTranscoderCallback, ¶ms); + } + + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + uint32_t size; + } _OrthancPluginCreateMemoryBuffer; + + /** + * @brief Create a memory buffer. + * + * This function creates a memory buffer that is managed by the + * Orthanc core. The main use case of this function is for plugins + * that act as DICOM transcoders. + * + * Your plugin should never call "free()" on the resulting memory + * buffer, as the C library that is used by the plugin is in general + * not the same as the one used by the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param size Size of the memory buffer to be created. + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateMemoryBuffer( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + uint32_t size) + { + _OrthancPluginCreateMemoryBuffer params; + params.target = target; + params.size = size; + + return context->InvokeService(context, _OrthancPluginService_CreateMemoryBuffer, ¶ms); + } + #ifdef __cplusplus } diff -r 66879215cbf3 -r 596912ebab5f Plugins/Samples/Common/OrthancPluginCppWrapper.cpp --- a/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Wed May 20 16:38:33 2020 +0200 +++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Wed May 20 17:03:24 2020 +0200 @@ -130,6 +130,28 @@ } +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + MemoryBuffer::MemoryBuffer(const void* buffer, + size_t size) + { + uint32_t s = static_cast(size); + if (static_cast(s) != size) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + else if (OrthancPluginCreateMemoryBuffer(GetGlobalContext(), &buffer_, s) != + OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + else + { + memcpy(buffer_.data, buffer, size); + } + } +#endif + + void MemoryBuffer::Clear() { if (buffer_.data != NULL) @@ -3364,7 +3386,9 @@ } else { - return new DicomInstance(instance); + boost::movelib::unique_ptr result(new DicomInstance(instance)); + result->toFree_ = true; + return result.release(); } } #endif diff -r 66879215cbf3 -r 596912ebab5f Plugins/Samples/Common/OrthancPluginCppWrapper.h --- a/Plugins/Samples/Common/OrthancPluginCppWrapper.h Wed May 20 16:38:33 2020 +0200 +++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.h Wed May 20 17:03:24 2020 +0200 @@ -146,6 +146,13 @@ public: MemoryBuffer(); +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + // This constructor makes a copy of the given buffer in the memory + // handled by the Orthanc core + MemoryBuffer(const void* buffer, + size_t size); +#endif + ~MemoryBuffer() { Clear(); diff -r 66879215cbf3 -r 596912ebab5f Plugins/Samples/GdcmDecoder/CMakeLists.txt --- a/Plugins/Samples/GdcmDecoder/CMakeLists.txt Wed May 20 16:38:33 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,83 +0,0 @@ -# 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 Affero General Public License -# as published by the Free Software Foundation, either version 3 of -# the License, or (at your option) any later version. -# -# 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 -# Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -cmake_minimum_required(VERSION 2.8) - -project(GdcmDecoder) - -SET(PLUGIN_VERSION "mainline" CACHE STRING "Version of the plugin") - - -# Parameters of the build -set(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") -set(STANDALONE_BUILD ON CACHE BOOL "Standalone build (all the resources are embedded, necessary for releases)") -set(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") - -# Advanced parameters to fine-tune linking against system libraries -set(USE_SYSTEM_GDCM ON CACHE BOOL "Use the system version of Grassroot DICOM (GDCM)") -set(USE_SYSTEM_ORTHANC_SDK ON CACHE BOOL "Use the system version of the Orthanc plugin SDK") - -# Setup the Orthanc framework -set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}/../../..) - -set(ORTHANC_FRAMEWORK_PLUGIN ON) -include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkParameters.cmake) - -set(ENABLE_LOCALE OFF CACHE INTERNAL "") # Disable support for locales (notably in Boost) -set(ENABLE_MODULE_IMAGES OFF CACHE INTERNAL "") -set(ENABLE_MODULE_JOBS OFF CACHE INTERNAL "") - -include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkConfiguration.cmake) - -include(GdcmConfiguration.cmake) - - -# Check that the Orthanc SDK headers are available -if (STATIC_BUILD OR NOT USE_SYSTEM_ORTHANC_SDK) - #include_directories(${CMAKE_SOURCE_DIR}/Resources/Orthanc/Sdk-0.9.5) - include_directories(${CMAKE_SOURCE_DIR}/../../Include) # TODO => SYNC 0.9.5 -else () - CHECK_INCLUDE_FILE_CXX(orthanc/OrthancCPlugin.h HAVE_ORTHANC_H) - if (NOT HAVE_ORTHANC_H) - message(FATAL_ERROR "Please install the headers of the Orthanc plugins SDK") - endif() -endif() - - -include_directories(${ORTHANC_ROOT}) - -add_definitions( - -DPLUGIN_VERSION="${PLUGIN_VERSION}" - -DHAS_ORTHANC_EXCEPTION=1 - -DORTHANC_ENABLE_LOGGING_PLUGIN=1 - ) - -add_library(GdcmDecoder SHARED - GdcmDecoderCache.cpp - GdcmImageDecoder.cpp - Plugin.cpp - ${CMAKE_SOURCE_DIR}/../Common/OrthancPluginCppWrapper.cpp - ${ORTHANC_CORE_SOURCES} - ) - -target_link_libraries(GdcmDecoder ${GDCM_LIBRARIES}) - -if (STATIC_BUILD OR NOT USE_SYSTEM_GDCM) - add_dependencies(GdcmDecoder GDCM) -endif() diff -r 66879215cbf3 -r 596912ebab5f Plugins/Samples/GdcmDecoder/GdcmConfiguration.cmake --- a/Plugins/Samples/GdcmDecoder/GdcmConfiguration.cmake Wed May 20 16:38:33 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,141 +0,0 @@ -# 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 Affero General Public License -# as published by the Free Software Foundation, either version 3 of -# the License, or (at your option) any later version. -# -# 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 -# Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -if (STATIC_BUILD OR NOT USE_SYSTEM_GDCM) - if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR - ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR - ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD") - # If using gcc, build GDCM with the "-fPIC" argument to allow its - # embedding into the shared library containing the Orthanc plugin - set(AdditionalCFlags "-fPIC") - set(AdditionalCxxFlags ${AdditionalCFlags}) - elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows" AND - CMAKE_COMPILER_IS_GNUCXX) - # Prevents error: "jump to label ‘err’ crosses initialization" of some variable - # within "Source/Common/gdcmCAPICryptographicMessageSyntax.cxx" if using MinGW - set(AdditionalCxxFlags "-fpermissive") - elseif (${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") - # This definition is necessary to compile - # "Source/MediaStorageAndFileFormat/gdcmFileStreamer.cxx" - set(AdditionalCFlags "-Doff64_t=off_t") - set(AdditionalCxxFlags ${AdditionalCFlags}) - endif() - - set(Flags - "-DCMAKE_C_FLAGS=${CMAKE_C_FLAGS} ${AdditionalCFlags}" - "-DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS} ${AdditionalCxxFlags}" - -DCMAKE_C_FLAGS_DEBUG=${CMAKE_C_FLAGS_DEBUG} - -DCMAKE_CXX_FLAGS_DEBUG=${CMAKE_CXX_FLAGS_DEBUG} - -DCMAKE_C_FLAGS_RELEASE=${CMAKE_C_FLAGS_RELEASE} - -DCMAKE_CXX_FLAGS_RELEASE=${CMAKE_CXX_FLAGS_RELEASE} - -DCMAKE_C_FLAGS_MINSIZEREL=${CMAKE_C_FLAGS_MINSIZEREL} - -DCMAKE_CXX_FLAGS_MINSIZEREL=${CMAKE_CXX_FLAGS_MINSIZEREL} - -DCMAKE_C_FLAGS_RELWITHDEBINFO=${CMAKE_C_FLAGS_RELWITHDEBINFO} - -DCMAKE_CXX_FLAGS_RELWITHDEBINFO=${CMAKE_CXX_FLAGS_RELWITHDEBINFO} - ) - - if (CMAKE_TOOLCHAIN_FILE) - # Take absolute path to the toolchain - get_filename_component(TMP ${CMAKE_TOOLCHAIN_FILE} REALPATH BASE ${CMAKE_SOURCE_DIR}) - list(APPEND Flags -DCMAKE_TOOLCHAIN_FILE=${TMP}) - endif() - - # Don't build manpages (since gdcm 2.8.4) - list(APPEND Flags -DGDCM_BUILD_DOCBOOK_MANPAGES=OFF) - - if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") - # Trick to disable the compilation of socket++ by gdcm, which is - # incompatible with LSB, but fortunately only required for DICOM - # Networking - list(APPEND Flags -DGDCM_USE_SYSTEM_SOCKETXX=ON) - - # Detect the number of CPU cores to run "make" with as much - # parallelism as possible - include(ProcessorCount) - ProcessorCount(N) - if (NOT N EQUAL 0) - set(MAKE_PARALLEL -j${N}) - endif() - - # For Linux Standard Base, avoid building incompatible target gdcmMEXD (*) - set(BUILD_COMMAND BUILD_COMMAND - ${CMAKE_MAKE_PROGRAM} ${MAKE_PARALLEL} - gdcmMSFF gdcmcharls gdcmDICT gdcmDSED gdcmIOD gdcmjpeg8 - gdcmjpeg12 gdcmjpeg16 gdcmopenjp2 gdcmzlib gdcmCommon gdcmexpat) - endif() - - include(ExternalProject) - externalproject_add(GDCM - URL "http://orthanc.osimis.io/ThirdPartyDownloads/gdcm-3.0.4.tar.gz" - URL_MD5 "f12dbded708356d5fa0b5ed37ccdb66e" - TIMEOUT 60 - CMAKE_ARGS -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} ${Flags} - ${BUILD_COMMAND} # Customize "make", only for Linux Standard Base (*) - INSTALL_COMMAND "" # Skip the install step - ) - - if(MSVC) - set(Suffix ".lib") - set(Prefix "") - else() - set(Suffix ".a") - list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix) - endif() - - set(GDCM_LIBRARIES - # WARNING: The order of the libraries below *is* important! - ${Prefix}gdcmMSFF${Suffix} - ${Prefix}gdcmcharls${Suffix} - ${Prefix}gdcmDICT${Suffix} - ${Prefix}gdcmDSED${Suffix} - ${Prefix}gdcmIOD${Suffix} - ${Prefix}gdcmjpeg8${Suffix} - ${Prefix}gdcmjpeg12${Suffix} - ${Prefix}gdcmjpeg16${Suffix} - ${Prefix}gdcmopenjp2${Suffix} - ${Prefix}gdcmzlib${Suffix} - ${Prefix}gdcmCommon${Suffix} - ${Prefix}gdcmexpat${Suffix} - - #${Prefix}socketxx${Suffix} - #${Prefix}gdcmMEXD${Suffix} # DICOM Networking, unneeded by Orthanc plugins - #${Prefix}gdcmgetopt${Suffix} - #${Prefix}gdcmuuid${Suffix} - ) - - ExternalProject_Get_Property(GDCM binary_dir) - include_directories(${binary_dir}/Source/Common) - link_directories(${binary_dir}/bin) - - ExternalProject_Get_Property(GDCM source_dir) - include_directories( - ${source_dir}/Source/Common - ${source_dir}/Source/MediaStorageAndFileFormat - ${source_dir}/Source/DataStructureAndEncodingDefinition - ) - -else() - find_package(GDCM REQUIRED) - if (GDCM_FOUND) - include(${GDCM_USE_FILE}) - set(GDCM_LIBRARIES gdcmCommon gdcmMSFF) - else(GDCM_FOUND) - message(FATAL_ERROR "Cannot find GDCM, did you set GDCM_DIR?") - endif(GDCM_FOUND) -endif() diff -r 66879215cbf3 -r 596912ebab5f Plugins/Samples/GdcmDecoder/GdcmDecoderCache.cpp --- a/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.cpp Wed May 20 16:38:33 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,97 +0,0 @@ -/** - * 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. - * - * 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 . - **/ - - -#include "GdcmDecoderCache.h" - -#include "../../../Core/Compatibility.h" - -namespace OrthancPlugins -{ - std::string GdcmDecoderCache::ComputeMd5(const void* dicom, - size_t size) - { - std::string result; - - char* md5 = OrthancPluginComputeMd5(OrthancPlugins::GetGlobalContext(), dicom, size); - - if (md5 == NULL) - { - throw std::runtime_error("Cannot compute MD5 hash"); - } - - bool ok = false; - try - { - result.assign(md5); - ok = true; - } - catch (...) - { - } - - OrthancPluginFreeString(OrthancPlugins::GetGlobalContext(), md5); - - if (!ok) - { - throw std::runtime_error("Not enough memory"); - } - else - { - return result; - } - } - - - OrthancImage* GdcmDecoderCache::Decode(const void* dicom, - const uint32_t size, - uint32_t frameIndex) - { - std::string md5 = ComputeMd5(dicom, size); - - // First check whether the previously decoded image is the same - // as this one - { - boost::mutex::scoped_lock lock(mutex_); - - if (decoder_.get() != NULL && - size_ == size && - md5_ == md5) - { - // This is the same image: Reuse the previous decoding - return new OrthancImage(decoder_->Decode(frameIndex)); - } - } - - // This is not the same image - std::unique_ptr decoder(new GdcmImageDecoder(dicom, size)); - std::unique_ptr image(new OrthancImage(decoder->Decode(frameIndex))); - - { - // Cache the newly created decoder for further use - boost::mutex::scoped_lock lock(mutex_); - decoder_.reset(decoder.release()); - size_ = size; - md5_ = md5; - } - - return image.release(); - } -} diff -r 66879215cbf3 -r 596912ebab5f Plugins/Samples/GdcmDecoder/GdcmDecoderCache.h --- a/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.h Wed May 20 16:38:33 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,53 +0,0 @@ -/** - * 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. - * - * 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 . - **/ - - -#pragma once - -#include "../../../Core/Compatibility.h" -#include "GdcmImageDecoder.h" -#include "../Common/OrthancPluginCppWrapper.h" - -#include - - -namespace OrthancPlugins -{ - class GdcmDecoderCache : public boost::noncopyable - { - private: - boost::mutex mutex_; - std::unique_ptr decoder_; - size_t size_; - std::string md5_; - - static std::string ComputeMd5(const void* dicom, - size_t size); - - public: - GdcmDecoderCache() : size_(0) - { - } - - OrthancImage* Decode(const void* dicom, - const uint32_t size, - uint32_t frameIndex); - }; -} diff -r 66879215cbf3 -r 596912ebab5f Plugins/Samples/GdcmDecoder/GdcmImageDecoder.cpp --- a/Plugins/Samples/GdcmDecoder/GdcmImageDecoder.cpp Wed May 20 16:38:33 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,408 +0,0 @@ -/** - * 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. - * - * 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 . - **/ - - -#include "GdcmImageDecoder.h" - -#include "../../../Core/Compatibility.h" - -#include -#include -#include -#include -#include -#include -#include - - -namespace OrthancPlugins -{ - struct GdcmImageDecoder::PImpl - { - const void* dicom_; - size_t size_; - - gdcm::ImageReader reader_; - std::unique_ptr lut_; - std::unique_ptr photometric_; - std::unique_ptr interleaved_; - std::string decoded_; - - PImpl(const void* dicom, - size_t size) : - dicom_(dicom), - size_(size) - { - } - - - const gdcm::DataSet& GetDataSet() const - { - return reader_.GetFile().GetDataSet(); - } - - - const gdcm::Image& GetImage() const - { - if (interleaved_.get() != NULL) - { - return interleaved_->GetOutput(); - } - - if (lut_.get() != NULL) - { - return lut_->GetOutput(); - } - - if (photometric_.get() != NULL) - { - return photometric_->GetOutput(); - } - - return reader_.GetImage(); - } - - - void Decode() - { - // Change photometric interpretation or apply LUT, if required - { - const gdcm::Image& image = GetImage(); - if (image.GetPixelFormat().GetSamplesPerPixel() == 1 && - image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::PALETTE_COLOR) - { - lut_.reset(new gdcm::ImageApplyLookupTable()); - lut_->SetInput(image); - if (!lut_->Apply()) - { - throw std::runtime_error( "GDCM cannot apply the lookup table"); - } - } - else if (image.GetPixelFormat().GetSamplesPerPixel() == 1) - { - if (image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::MONOCHROME1 && - image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::MONOCHROME2) - { - photometric_.reset(new gdcm::ImageChangePhotometricInterpretation()); - photometric_->SetInput(image); - photometric_->SetPhotometricInterpretation(gdcm::PhotometricInterpretation::MONOCHROME2); - if (!photometric_->Change() || - GetImage().GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::MONOCHROME2) - { - throw std::runtime_error("GDCM cannot change the photometric interpretation"); - } - } - } - else - { - if (image.GetPixelFormat().GetSamplesPerPixel() == 3 && - image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::RGB && - image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::YBR_FULL && - (image.GetTransferSyntax() != gdcm::TransferSyntax::JPEG2000Lossless || - image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::YBR_RCT)) - { - photometric_.reset(new gdcm::ImageChangePhotometricInterpretation()); - photometric_->SetInput(image); - photometric_->SetPhotometricInterpretation(gdcm::PhotometricInterpretation::RGB); - if (!photometric_->Change() || - GetImage().GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::RGB) - { - throw std::runtime_error("GDCM cannot change the photometric interpretation"); - } - } - } - } - - // Possibly convert planar configuration to interleaved - { - const gdcm::Image& image = GetImage(); - if (image.GetPlanarConfiguration() != 0 && - image.GetPixelFormat().GetSamplesPerPixel() != 1) - { - interleaved_.reset(new gdcm::ImageChangePlanarConfiguration()); - interleaved_->SetInput(image); - if (!interleaved_->Change() || - GetImage().GetPlanarConfiguration() != 0) - { - throw std::runtime_error("GDCM cannot change the planar configuration to interleaved"); - } - } - } - } - }; - - GdcmImageDecoder::GdcmImageDecoder(const void* dicom, - size_t size) : - pimpl_(new PImpl(dicom, size)) - { - // Setup a stream to the memory buffer - using namespace boost::iostreams; - basic_array_source source(reinterpret_cast(dicom), size); - stream > stream(source); - - // Parse the DICOM instance using GDCM - pimpl_->reader_.SetStream(stream); - if (!pimpl_->reader_.Read()) - { - throw std::runtime_error("Bad file format"); - } - - pimpl_->Decode(); - } - - - OrthancPluginPixelFormat GdcmImageDecoder::GetFormat() const - { - const gdcm::Image& image = pimpl_->GetImage(); - - if (image.GetPixelFormat().GetSamplesPerPixel() == 1 && - (image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::MONOCHROME1 || - image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::MONOCHROME2)) - { - switch (image.GetPixelFormat()) - { - case gdcm::PixelFormat::UINT16: - return OrthancPluginPixelFormat_Grayscale16; - - case gdcm::PixelFormat::INT16: - return OrthancPluginPixelFormat_SignedGrayscale16; - - case gdcm::PixelFormat::UINT8: - return OrthancPluginPixelFormat_Grayscale8; - - default: - throw std::runtime_error("Unsupported pixel format"); - } - } - else if (image.GetPixelFormat().GetSamplesPerPixel() == 3 && - (image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::RGB || - image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::YBR_FULL || - image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::YBR_RCT)) - { - switch (image.GetPixelFormat()) - { - case gdcm::PixelFormat::UINT8: - return OrthancPluginPixelFormat_RGB24; - - case gdcm::PixelFormat::UINT16: -#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 3, 1) - return OrthancPluginPixelFormat_RGB48; -#else - throw std::runtime_error("RGB48 pixel format is only supported if compiled against Orthanc SDK >= 1.3.1"); -#endif - - default: - break; - } - } - - throw std::runtime_error("Unsupported pixel format"); - } - - - unsigned int GdcmImageDecoder::GetWidth() const - { - return pimpl_->GetImage().GetColumns(); - } - - - unsigned int GdcmImageDecoder::GetHeight() const - { - return pimpl_->GetImage().GetRows(); - } - - - unsigned int GdcmImageDecoder::GetFramesCount() const - { - return pimpl_->GetImage().GetDimension(2); - } - - - size_t GdcmImageDecoder::GetBytesPerPixel(OrthancPluginPixelFormat format) - { - switch (format) - { - case OrthancPluginPixelFormat_Grayscale8: - return 1; - - case OrthancPluginPixelFormat_Grayscale16: - case OrthancPluginPixelFormat_SignedGrayscale16: - return 2; - - case OrthancPluginPixelFormat_RGB24: - return 3; - -#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 3, 1) - case OrthancPluginPixelFormat_RGB48: - return 6; -#endif - - default: - throw std::runtime_error("Unsupport pixel format"); - } - } - - static void ConvertYbrToRgb(uint8_t rgb[3], - const uint8_t ybr[3]) - { - // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.3.html#sect_C.7.6.3.1.2 - // https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion - - // TODO - Check out the outcome of Mathieu's discussion about - // truncation of YCbCr-to-RGB conversion: - // https://groups.google.com/forum/#!msg/comp.protocols.dicom/JHuGeyWbTz8/ARoTWrJzAQAJ - - const float Y = ybr[0]; - const float Cb = ybr[1]; - const float Cr = ybr[2]; - - const float result[3] = { - Y + 1.402f * (Cr - 128.0f), - Y - 0.344136f * (Cb - 128.0f) - 0.714136f * (Cr - 128.0f), - Y + 1.772f * (Cb - 128.0f) - }; - - for (uint8_t i = 0; i < 3 ; i++) - { - if (result[i] < 0) - { - rgb[i] = 0; - } - else if (result[i] > 255) - { - rgb[i] = 255; - } - else - { - rgb[i] = static_cast(result[i]); - } - } - } - - - static void FixPhotometricInterpretation(OrthancImage& image, - gdcm::PhotometricInterpretation interpretation) - { - switch (interpretation) - { - case gdcm::PhotometricInterpretation::MONOCHROME1: - case gdcm::PhotometricInterpretation::MONOCHROME2: - case gdcm::PhotometricInterpretation::RGB: - return; - - case gdcm::PhotometricInterpretation::YBR_FULL: - { - // Fix for Osimis issue WVB-319: Some images are not loading in US_MF - - uint32_t width = image.GetWidth(); - uint32_t height = image.GetHeight(); - uint32_t pitch = image.GetPitch(); - uint8_t* buffer = reinterpret_cast(image.GetBuffer()); - - if (image.GetPixelFormat() != OrthancPluginPixelFormat_RGB24 || - pitch < 3 * width) - { - throw std::runtime_error("Internal error"); - } - - for (uint32_t y = 0; y < height; y++) - { - uint8_t* p = buffer + y * pitch; - for (uint32_t x = 0; x < width; x++, p += 3) - { - const uint8_t ybr[3] = { p[0], p[1], p[2] }; - uint8_t rgb[3]; - ConvertYbrToRgb(rgb, ybr); - p[0] = rgb[0]; - p[1] = rgb[1]; - p[2] = rgb[2]; - } - } - - return; - } - - default: - throw std::runtime_error("Unsupported output photometric interpretation"); - } - } - - - OrthancPluginImage* GdcmImageDecoder::Decode(unsigned int frameIndex) const - { - unsigned int frames = GetFramesCount(); - unsigned int width = GetWidth(); - unsigned int height = GetHeight(); - OrthancPluginPixelFormat format = GetFormat(); - size_t bpp = GetBytesPerPixel(format); - - if (frameIndex >= frames) - { - throw std::runtime_error("Inexistent frame index"); - } - - std::string& decoded = pimpl_->decoded_; - OrthancImage target(format, width, height); - - if (width == 0 || - height == 0) - { - return target.Release(); - } - - if (decoded.empty()) - { - decoded.resize(pimpl_->GetImage().GetBufferLength()); - if (!pimpl_->GetImage().GetBuffer(&decoded[0])) - { - throw std::runtime_error("Image not properly decoded to a memory buffer"); - } - } - - const void* sourceBuffer = &decoded[0]; - - if (target.GetPitch() == bpp * width && - frames == 1) - { - assert(decoded.size() == target.GetPitch() * target.GetHeight()); - memcpy(target.GetBuffer(), sourceBuffer, decoded.size()); - } - else - { - size_t targetPitch = target.GetPitch(); - size_t sourcePitch = width * bpp; - - const uint8_t* a = (reinterpret_cast(decoded.c_str()) + - sourcePitch * height * frameIndex); - uint8_t* b = reinterpret_cast(target.GetBuffer()); - - for (uint32_t y = 0; y < height; y++) - { - memcpy(b, a, sourcePitch); - a += sourcePitch; - b += targetPitch; - } - } - - FixPhotometricInterpretation(target, pimpl_->GetImage().GetPhotometricInterpretation()); - - return target.Release(); - } -} diff -r 66879215cbf3 -r 596912ebab5f Plugins/Samples/GdcmDecoder/GdcmImageDecoder.h --- a/Plugins/Samples/GdcmDecoder/GdcmImageDecoder.h Wed May 20 16:38:33 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,55 +0,0 @@ -/** - * 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. - * - * 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 . - **/ - - -#pragma once - -#include "../Common/OrthancPluginCppWrapper.h" - -#include -#include -#include - - -namespace OrthancPlugins -{ - class GdcmImageDecoder : public boost::noncopyable - { - private: - struct PImpl; - boost::shared_ptr pimpl_; - - public: - GdcmImageDecoder(const void* dicom, - size_t size); - - OrthancPluginPixelFormat GetFormat() const; - - unsigned int GetWidth() const; - - unsigned int GetHeight() const; - - unsigned int GetFramesCount() const; - - static size_t GetBytesPerPixel(OrthancPluginPixelFormat format); - - OrthancPluginImage* Decode(unsigned int frameIndex) const; - }; -} diff -r 66879215cbf3 -r 596912ebab5f Plugins/Samples/GdcmDecoder/Plugin.cpp --- a/Plugins/Samples/GdcmDecoder/Plugin.cpp Wed May 20 16:38:33 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,249 +0,0 @@ -/** - * 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. - * - * 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 . - **/ - - -#include "../../../Core/Compatibility.h" -#include "../../../Core/DicomFormat/DicomMap.h" -#include "../../../Core/Toolbox.h" -#include "GdcmDecoderCache.h" - -static OrthancPlugins::GdcmDecoderCache cache_; -static bool restrictTransferSyntaxes_ = false; -static std::set enabledTransferSyntaxes_; - - -static bool ExtractTransferSyntax(std::string& transferSyntax, - const void* dicom, - const uint32_t size) -{ - Orthanc::DicomMap header; - if (!Orthanc::DicomMap::ParseDicomMetaInformation(header, reinterpret_cast(dicom), size)) - { - return false; - } - - const Orthanc::DicomValue* tag = header.TestAndGetValue(0x0002, 0x0010); - if (tag == NULL || - tag->IsNull() || - tag->IsBinary()) - { - return false; - } - else - { - // Stripping spaces should not be required, as this is a UI value - // representation whose stripping is supported by the Orthanc - // core, but let's be careful... - transferSyntax = Orthanc::Toolbox::StripSpaces(tag->GetContent()); - return true; - } -} - - -static bool IsTransferSyntaxEnabled(const void* dicom, - const uint32_t size) -{ - std::string formattedSize; - - { - char tmp[16]; - sprintf(tmp, "%0.1fMB", static_cast(size) / (1024.0f * 1024.0f)); - formattedSize.assign(tmp); - } - - if (!restrictTransferSyntaxes_) - { - LOG(INFO) << "Decoding one DICOM instance of " << formattedSize << " using GDCM"; - return true; - } - - std::string transferSyntax; - if (!ExtractTransferSyntax(transferSyntax, dicom, size)) - { - LOG(INFO) << "Cannot extract the transfer syntax of this instance of " - << formattedSize << ", will use GDCM to decode it"; - return true; - } - else if (enabledTransferSyntaxes_.find(transferSyntax) != enabledTransferSyntaxes_.end()) - { - // Decoding for this transfer syntax is enabled - LOG(INFO) << "Using GDCM to decode this instance of " << formattedSize - << " with transfer syntax " << transferSyntax; - return true; - } - else - { - LOG(INFO) << "Won't use GDCM to decode this instance of " << formattedSize - << ", as its transfer syntax " << transferSyntax << " is disabled"; - return false; - } -} - - -static OrthancPluginErrorCode DecodeImageCallback(OrthancPluginImage** target, - const void* dicom, - const uint32_t size, - uint32_t frameIndex) -{ - try - { - if (!IsTransferSyntaxEnabled(dicom, size)) - { - *target = NULL; - return OrthancPluginErrorCode_Success; - } - - std::unique_ptr image; - -#if 0 - // Do not use the cache - OrthancPlugins::GdcmImageDecoder decoder(dicom, size); - image.reset(new OrthancPlugins::OrthancImage(decoder.Decode(frameIndex))); -#else - image.reset(cache_.Decode(dicom, size, frameIndex)); -#endif - - *target = image->Release(); - - return OrthancPluginErrorCode_Success; - } - catch (Orthanc::OrthancException& e) - { - *target = NULL; - - LOG(WARNING) << "Cannot decode image using GDCM: " << e.What(); - return OrthancPluginErrorCode_Plugin; - } - catch (std::runtime_error& e) - { - *target = NULL; - - LOG(WARNING) << "Cannot decode image using GDCM: " << e.what(); - return OrthancPluginErrorCode_Plugin; - } - catch (...) - { - *target = NULL; - - LOG(WARNING) << "Native exception while decoding image using GDCM"; - return OrthancPluginErrorCode_Plugin; - } -} - - - -/** - * We force the redefinition of the "ORTHANC_PLUGINS_API" macro, that - * was left empty with gcc until Orthanc SDK 1.5.7 (no "default" - * visibility). This causes the version script, if run from "Holy - * Build Box", to make private the 4 global functions of the plugin. - **/ - -#undef ORTHANC_PLUGINS_API - -#ifdef WIN32 -# define ORTHANC_PLUGINS_API __declspec(dllexport) -#elif __GNUC__ >= 4 -# define ORTHANC_PLUGINS_API __attribute__ ((visibility ("default"))) -#else -# define ORTHANC_PLUGINS_API -#endif - - -extern "C" -{ - ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) - { - static const char* const KEY_GDCM = "Gdcm"; - static const char* const KEY_ENABLE_GDCM = "EnableGdcm"; - static const char* const KEY_RESTRICT_TRANSFER_SYNTAXES = "RestrictTransferSyntaxes"; - - OrthancPlugins::SetGlobalContext(context); - LOG(INFO) << "Initializing the advanced decoder of medical images using GDCM"; - - - /* Check the version of the Orthanc core */ - if (OrthancPluginCheckVersion(context) == 0) - { - char info[1024]; - sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin", - context->orthancVersion, - ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, - ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, - ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); - OrthancPluginLogError(context, info); - return -1; - } - - OrthancPluginSetDescription(context, "Advanced decoder of medical images using GDCM."); - - OrthancPlugins::OrthancConfiguration global; - - bool enabled = true; - - if (global.IsSection(KEY_GDCM)) - { - OrthancPlugins::OrthancConfiguration config; - global.GetSection(config, KEY_GDCM); - - enabled = config.GetBooleanValue(KEY_ENABLE_GDCM, true); - - if (config.LookupSetOfStrings(enabledTransferSyntaxes_, KEY_RESTRICT_TRANSFER_SYNTAXES, false)) - { - restrictTransferSyntaxes_ = true; - - for (std::set::const_iterator it = enabledTransferSyntaxes_.begin(); - it != enabledTransferSyntaxes_.end(); ++it) - { - LOG(WARNING) << "Orthanc will use GDCM to decode transfer syntax: " << *it; - } - } - } - - if (enabled) - { - OrthancPluginRegisterDecodeImageCallback(context, DecodeImageCallback); - } - else - { - LOG(WARNING) << "The advanced decoder of medical images using GDCM is disabled"; - } - - return 0; - } - - - ORTHANC_PLUGINS_API void OrthancPluginFinalize() - { - LOG(INFO) << "Finalizing the advanced decoder of medical images using GDCM"; - } - - - ORTHANC_PLUGINS_API const char* OrthancPluginGetName() - { - return "gdcm-decoder"; - } - - - ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() - { - return PLUGIN_VERSION; - } -} diff -r 66879215cbf3 -r 596912ebab5f Plugins/Samples/GdcmDecoder/README --- a/Plugins/Samples/GdcmDecoder/README Wed May 20 16:38:33 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,6 +0,0 @@ -This sample shows how to replace the decoder of DICOM images that is -built in Orthanc, by the GDCM library. - -A production-ready version of this sample, is available in the -offical Web viewer plugin: -http://www.orthanc-server.com/static.php?page=web-viewer diff -r 66879215cbf3 -r 596912ebab5f Resources/Configuration.json --- a/Resources/Configuration.json Wed May 20 16:38:33 2020 +0200 +++ b/Resources/Configuration.json Wed May 20 17:03:24 2020 +0200 @@ -547,5 +547,30 @@ // Whether Orthanc transcodes DICOM files to an uncompressed // transfer syntax over the DICOM protocol, if the remote modality // does not support compressed transfer syntaxes (new in Orthanc 1.7.0). - "TranscodeDicomProtocol" : true + "TranscodeDicomProtocol" : true, + + // If some plugin to decode/transcode DICOM instances is installed, + // this option specifies whether the built-in decoder/transcoder of + // Orthanc (that uses DCMTK) is applied before or after the plugins, + // or is not applied at all (new in Orthanc 1.7.0). The allowed + // values for this option are "After" (default value, corresponding + // to the behavior of Orthanc <= 1.6.1), "Before", or "Disabled". + "BuiltinDecoderTranscoderOrder" : "After", + + // If this option is set, Orthanc will transparently transcode any + // incoming DICOM instance to the given transfer syntax before + // storing it into its database. Beware that this might result in + // high CPU usage (if transcoding to some compressed transfer + // syntax), or in higher disk consumption (if transcoding to an + // uncompressed syntax). Also, beware that transcoding to a transfer + // syntax with lossy compression (notably JPEG) will change the + // "SOPInstanceUID" DICOM tag, and thus the Orthanc identifier at + // the instance level, which might break external workflow. + /** + "IngestTranscoding" : "1.2.840.10008.1.2", + **/ + + // The compression level that is used when transcoding to one of the + // lossy/JPEG transfer syntaxes (integer between 1 and 100). + "DicomLossyTranscodingQuality" : 90 } diff -r 66879215cbf3 -r 596912ebab5f Resources/Orthanc.doxygen --- a/Resources/Orthanc.doxygen Wed May 20 16:38:33 2020 +0200 +++ b/Resources/Orthanc.doxygen Wed May 20 17:03:24 2020 +0200 @@ -545,7 +545,7 @@ # this will also influence the order of the classes in the class list. # The default value is: NO. -SORT_BRIEF_DOCS = NO +SORT_BRIEF_DOCS = YES # If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the # (brief and detailed) documentation of class members so that constructors and diff -r 66879215cbf3 -r 596912ebab5f Resources/OrthancPlugin.doxygen --- a/Resources/OrthancPlugin.doxygen Wed May 20 16:38:33 2020 +0200 +++ b/Resources/OrthancPlugin.doxygen Wed May 20 17:03:24 2020 +0200 @@ -545,7 +545,7 @@ # this will also influence the order of the classes in the class list. # The default value is: NO. -SORT_BRIEF_DOCS = NO +SORT_BRIEF_DOCS = YES # If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the # (brief and detailed) documentation of class members so that constructors and diff -r 66879215cbf3 -r 596912ebab5f UnitTestsSources/FromDcmtkTests.cpp --- a/UnitTestsSources/FromDcmtkTests.cpp Wed May 20 16:38:33 2020 +0200 +++ b/UnitTestsSources/FromDcmtkTests.cpp Wed May 20 17:03:24 2020 +0200 @@ -513,7 +513,8 @@ f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "Tata"); // (**) std::string s; - ASSERT_FALSE(f.LookupTransferSyntax(s)); + ASSERT_TRUE(f.LookupTransferSyntax(s)); + ASSERT_EQ(s, GetTransferSyntaxUid(DicomTransferSyntax_LittleEndianExplicit)); ASSERT_THROW(f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), false, DicomReplaceMode_ThrowIfAbsent, ""), OrthancException); @@ -1979,6 +1980,8 @@ Orthanc::SystemToolbox::ReadFile(source, "/home/jodogne/Subversion/orthanc-tests/Database/KarstenHilbertRF.dcm"); std::unique_ptr toto(FromDcmtkBridge::LoadFromMemoryBuffer(source.c_str(), source.size())); + const std::string sourceUid = IDicomTranscoder::GetSopInstanceUid(*toto); + DicomTransferSyntax sourceSyntax; ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, *toto)); @@ -1987,14 +1990,16 @@ for (int i = 0; i <= DicomTransferSyntax_XML; i++) { DicomTransferSyntax a = (DicomTransferSyntax) i; + + std::set s; + s.insert(a); std::string t; - bool hasSopInstanceUidChanged; - DicomTransferSyntax sourceSyntax2; + IDicomTranscoder::DicomImage source, target; + source.AcquireParsed(dynamic_cast(toto->clone())); - std::unique_ptr cloned(dynamic_cast(toto->clone())); - if (!transcoder.TranscodeParsedToBuffer(t, sourceSyntax2, hasSopInstanceUidChanged, *cloned, a, true)) + if (!transcoder.Transcode(target, source, s, true)) { printf("**************** CANNOT: [%s] => [%s]\n", GetTransferSyntaxUid(sourceSyntax), GetTransferSyntaxUid(a)); @@ -2002,22 +2007,21 @@ else { DicomTransferSyntax targetSyntax; - ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax, *cloned)); + ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax, target.GetParsed())); ASSERT_EQ(targetSyntax, a); - ASSERT_EQ(sourceSyntax, sourceSyntax2); bool lossy = (a == DicomTransferSyntax_JPEGProcess1 || a == DicomTransferSyntax_JPEGProcess2_4 || a == DicomTransferSyntax_JPEGLSLossy); printf("SIZE: %lu\n", t.size()); - if (hasSopInstanceUidChanged) + if (sourceUid == IDicomTranscoder::GetSopInstanceUid(target.GetParsed())) { - ASSERT_TRUE(lossy); + ASSERT_FALSE(lossy); } else { - ASSERT_FALSE(lossy); + ASSERT_TRUE(lossy); } } } diff -r 66879215cbf3 -r 596912ebab5f UnitTestsSources/MultiThreadingTests.cpp --- a/UnitTestsSources/MultiThreadingTests.cpp Wed May 20 16:38:33 2020 +0200 +++ b/UnitTestsSources/MultiThreadingTests.cpp Wed May 20 17:03:24 2020 +0200 @@ -1549,6 +1549,30 @@ ASSERT_EQ("username", tmp.GetPeer().GetUsername()); ASSERT_EQ("password", tmp.GetPeer().GetPassword()); ASSERT_TRUE(tmp.GetPeer().IsPkcs11Enabled()); + ASSERT_FALSE(tmp.IsTranscode()); + ASSERT_THROW(tmp.GetTransferSyntax(), OrthancException); + } + + { + OrthancPeerStoreJob job(GetContext()); + ASSERT_THROW(job.SetTranscode("nope"), OrthancException); + job.SetTranscode("1.2.840.10008.1.2.4.50"); + + ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); + ASSERT_TRUE(job.Serialize(s)); + } + + { + std::unique_ptr job; + job.reset(unserializer.UnserializeJob(s)); + + OrthancPeerStoreJob& tmp = dynamic_cast(*job); + ASSERT_EQ("http://127.0.0.1:8042/", tmp.GetPeer().GetUrl()); + ASSERT_EQ("", tmp.GetPeer().GetUsername()); + ASSERT_EQ("", tmp.GetPeer().GetPassword()); + ASSERT_FALSE(tmp.GetPeer().IsPkcs11Enabled()); + ASSERT_TRUE(tmp.IsTranscode()); + ASSERT_EQ(DicomTransferSyntax_JPEGProcess1, tmp.GetTransferSyntax()); } // ResourceModificationJob @@ -1560,7 +1584,8 @@ ResourceModificationJob job(GetContext()); job.SetModification(modification.release(), ResourceType_Patient, true); job.SetOrigin(DicomInstanceOrigin::FromLua()); - + + job.AddTrailingStep(); // Necessary since 1.7.0 ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); ASSERT_TRUE(job.Serialize(s)); } @@ -1571,10 +1596,33 @@ ResourceModificationJob& tmp = dynamic_cast(*job); ASSERT_TRUE(tmp.IsAnonymization()); + ASSERT_FALSE(tmp.IsTranscode()); + ASSERT_THROW(tmp.GetTransferSyntax(), OrthancException); ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin()); ASSERT_TRUE(tmp.GetModification().IsRemoved(DICOM_TAG_STUDY_DESCRIPTION)); } + { + ResourceModificationJob job(GetContext()); + ASSERT_THROW(job.SetTranscode("nope"), OrthancException); + job.SetTranscode(DicomTransferSyntax_JPEGProcess1); + + job.AddTrailingStep(); // Necessary since 1.7.0 + ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); + ASSERT_TRUE(job.Serialize(s)); + } + + { + std::unique_ptr job; + job.reset(unserializer.UnserializeJob(s)); + + ResourceModificationJob& tmp = dynamic_cast(*job); + ASSERT_FALSE(tmp.IsAnonymization()); + ASSERT_TRUE(tmp.IsTranscode()); + ASSERT_EQ(DicomTransferSyntax_JPEGProcess1, tmp.GetTransferSyntax()); + ASSERT_EQ(RequestOrigin_Unknown, tmp.GetOrigin().GetRequestOrigin()); + } + // SplitStudyJob std::string instance;