3597 lines
106 KiB
C++
3597 lines
106 KiB
C++
/**
|
|
* Orthanc - A Lightweight, RESTful DICOM Store
|
|
* Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
|
* Department, University Hospital of Liege, Belgium
|
|
* Copyright (C) 2017-2023 Osimis S.A., Belgium
|
|
* Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
|
|
* Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
|
|
*
|
|
* This program is free software: you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser 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
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this program. If not, see
|
|
* <http://www.gnu.org/licenses/>.
|
|
**/
|
|
|
|
|
|
#include "../PrecompiledHeaders.h"
|
|
|
|
#ifndef NOMINMAX
|
|
#define NOMINMAX
|
|
#endif
|
|
|
|
#if !defined(ORTHANC_SANDBOXED)
|
|
# error The macro ORTHANC_SANDBOXED must be defined
|
|
#endif
|
|
|
|
#if !defined(DCMTK_VERSION_NUMBER)
|
|
# error The macro DCMTK_VERSION_NUMBER must be defined
|
|
#endif
|
|
|
|
#include "FromDcmtkBridge.h"
|
|
#include "ToDcmtkBridge.h"
|
|
#include "../ChunkedBuffer.h"
|
|
#include "../Compatibility.h"
|
|
#include "../Logging.h"
|
|
#include "../Toolbox.h"
|
|
#include "../OrthancException.h"
|
|
|
|
#if ORTHANC_SANDBOXED == 0
|
|
# include "../TemporaryFile.h"
|
|
#endif
|
|
|
|
#include <list>
|
|
#include <limits>
|
|
|
|
#include <boost/lexical_cast.hpp>
|
|
#include <boost/filesystem.hpp>
|
|
#include <boost/algorithm/string/predicate.hpp>
|
|
#include <boost/algorithm/string/join.hpp>
|
|
|
|
#include <dcmtk/dcmdata/dcdeftag.h>
|
|
#include <dcmtk/dcmdata/dcdicent.h>
|
|
#include <dcmtk/dcmdata/dcfilefo.h>
|
|
#include <dcmtk/dcmdata/dcistrmb.h>
|
|
#include <dcmtk/dcmdata/dcostrmb.h>
|
|
#include <dcmtk/dcmdata/dcpixel.h>
|
|
#include <dcmtk/dcmdata/dcuid.h>
|
|
#include <dcmtk/dcmdata/dcxfer.h>
|
|
|
|
#include <dcmtk/dcmdata/dcvrae.h>
|
|
#include <dcmtk/dcmdata/dcvras.h>
|
|
#include <dcmtk/dcmdata/dcvrat.h>
|
|
#include <dcmtk/dcmdata/dcvrcs.h>
|
|
#include <dcmtk/dcmdata/dcvrda.h>
|
|
#include <dcmtk/dcmdata/dcvrds.h>
|
|
#include <dcmtk/dcmdata/dcvrdt.h>
|
|
#include <dcmtk/dcmdata/dcvrfd.h>
|
|
#include <dcmtk/dcmdata/dcvrfl.h>
|
|
#include <dcmtk/dcmdata/dcvris.h>
|
|
#include <dcmtk/dcmdata/dcvrlo.h>
|
|
#include <dcmtk/dcmdata/dcvrlt.h>
|
|
#include <dcmtk/dcmdata/dcvrpn.h>
|
|
#include <dcmtk/dcmdata/dcvrsh.h>
|
|
#include <dcmtk/dcmdata/dcvrsl.h>
|
|
#include <dcmtk/dcmdata/dcvrss.h>
|
|
#include <dcmtk/dcmdata/dcvrst.h>
|
|
#include <dcmtk/dcmdata/dcvrtm.h>
|
|
#include <dcmtk/dcmdata/dcvrui.h>
|
|
#include <dcmtk/dcmdata/dcvrul.h>
|
|
#include <dcmtk/dcmdata/dcvrus.h>
|
|
#include <dcmtk/dcmdata/dcvrut.h>
|
|
|
|
#if DCMTK_VERSION_NUMBER >= 361
|
|
# include <dcmtk/dcmdata/dcvruc.h>
|
|
# include <dcmtk/dcmdata/dcvrur.h>
|
|
#endif
|
|
|
|
#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
|
|
# if !defined(ORTHANC_FRAMEWORK_INCLUDE_RESOURCES) || (ORTHANC_FRAMEWORK_INCLUDE_RESOURCES == 1)
|
|
# include <OrthancFrameworkResources.h>
|
|
# endif
|
|
#endif
|
|
|
|
#if ORTHANC_ENABLE_DCMTK_JPEG == 1
|
|
# include <dcmtk/dcmjpeg/djdecode.h>
|
|
# if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
|
|
# include <dcmtk/dcmjpeg/djencode.h>
|
|
# endif
|
|
#endif
|
|
|
|
#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
|
|
# include <dcmtk/dcmjpls/djdecode.h>
|
|
# if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
|
|
# include <dcmtk/dcmjpls/djencode.h>
|
|
# endif
|
|
#endif
|
|
|
|
|
|
#include <dcmtk/dcmdata/dcrledrg.h>
|
|
#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
|
|
# include <dcmtk/dcmdata/dcrleerg.h>
|
|
# include <dcmtk/dcmimage/diregist.h> // include to support color images
|
|
#endif
|
|
|
|
|
|
static bool hasExternalDictionaries_ = false;
|
|
|
|
|
|
namespace Orthanc
|
|
{
|
|
FromDcmtkBridge::DictionaryWriterLock::DictionaryWriterLock() :
|
|
dictionary_(dcmDataDict.wrlock())
|
|
{
|
|
}
|
|
|
|
|
|
FromDcmtkBridge::DictionaryWriterLock::~DictionaryWriterLock()
|
|
{
|
|
#if DCMTK_VERSION_NUMBER >= 364
|
|
dcmDataDict.wrunlock();
|
|
#else
|
|
dcmDataDict.unlock();
|
|
#endif
|
|
}
|
|
|
|
|
|
FromDcmtkBridge::DictionaryReaderLock::DictionaryReaderLock() :
|
|
dictionary_(dcmDataDict.rdlock())
|
|
{
|
|
}
|
|
|
|
|
|
FromDcmtkBridge::DictionaryReaderLock::~DictionaryReaderLock()
|
|
{
|
|
#if DCMTK_VERSION_NUMBER >= 364
|
|
dcmDataDict.rdunlock();
|
|
#else
|
|
dcmDataDict.unlock();
|
|
#endif
|
|
}
|
|
|
|
|
|
static bool IsBinaryTag(const DcmTag& key)
|
|
{
|
|
return (key.isUnknownVR() ||
|
|
key.getEVR() == EVR_OB ||
|
|
key.getEVR() == EVR_OW ||
|
|
key.getEVR() == EVR_UN ||
|
|
key.getEVR() == EVR_ox);
|
|
}
|
|
|
|
|
|
#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
|
|
static void LoadEmbeddedDictionary(DcmDataDictionary& dictionary,
|
|
FrameworkResources::FileResourceId resource)
|
|
{
|
|
std::string content;
|
|
FrameworkResources::GetFileResource(content, resource);
|
|
|
|
#if ORTHANC_SANDBOXED == 0
|
|
TemporaryFile tmp;
|
|
tmp.Write(content);
|
|
|
|
if (!dictionary.loadDictionary(tmp.GetPath().c_str()))
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError,
|
|
"Cannot read embedded dictionary. Under Windows, make sure that "
|
|
"your TEMP directory does not contain special characters.");
|
|
}
|
|
#else
|
|
if (!dictionary.loadFromMemory(content))
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError,
|
|
"Cannot read embedded dictionary. Under Windows, make sure that "
|
|
"your TEMP directory does not contain special characters.");
|
|
}
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
|
|
namespace
|
|
{
|
|
class ChunkedBufferStream : public DcmOutputStream
|
|
{
|
|
private:
|
|
class Consumer : public DcmConsumer
|
|
{
|
|
private:
|
|
ChunkedBuffer buffer_;
|
|
|
|
public:
|
|
void Flatten(std::string& buffer)
|
|
{
|
|
buffer_.Flatten(buffer);
|
|
}
|
|
|
|
OFBool good() const ORTHANC_OVERRIDE
|
|
{
|
|
return true;
|
|
}
|
|
|
|
OFCondition status() const ORTHANC_OVERRIDE
|
|
{
|
|
return EC_Normal;
|
|
}
|
|
|
|
OFBool isFlushed() const ORTHANC_OVERRIDE
|
|
{
|
|
return true;
|
|
}
|
|
|
|
offile_off_t avail() const ORTHANC_OVERRIDE
|
|
{
|
|
// since we cannot report "unlimited", let's claim that we can still write 10MB.
|
|
// Note that offile_off_t is a signed type.
|
|
return 10 * 1024 * 1024;
|
|
}
|
|
|
|
offile_off_t write(const void *buf,
|
|
offile_off_t buflen) ORTHANC_OVERRIDE
|
|
{
|
|
buffer_.AddChunk(buf, buflen);
|
|
return buflen;
|
|
}
|
|
|
|
void flush() ORTHANC_OVERRIDE
|
|
{
|
|
// Nothing to flush
|
|
}
|
|
};
|
|
|
|
Consumer consumer_;
|
|
|
|
public:
|
|
ChunkedBufferStream() :
|
|
DcmOutputStream(&consumer_)
|
|
{
|
|
}
|
|
|
|
void Flatten(std::string& buffer)
|
|
{
|
|
consumer_.Flatten(buffer);
|
|
}
|
|
};
|
|
}
|
|
|
|
|
|
namespace
|
|
{
|
|
ORTHANC_FORCE_INLINE
|
|
static std::string FloatToString(float v)
|
|
{
|
|
/**
|
|
* From "boost::lexical_cast" documentation: "For more involved
|
|
* conversions, such as where precision or formatting need tighter
|
|
* control than is offered by the default behavior of
|
|
* lexical_cast, the conventional stringstream approach is
|
|
* recommended."
|
|
* https://www.boost.org/doc/libs/1_65_0/doc/html/boost_lexical_cast.html
|
|
* http://www.gotw.ca/publications/mill19.htm
|
|
*
|
|
* The precision of 17 corresponds to "defaultRealPrecision" in JsonCpp:
|
|
* https://github.com/open-source-parsers/jsoncpp/blob/master/include/json/value.h
|
|
**/
|
|
|
|
//return boost::lexical_cast<std::string>(v); // This was used in Orthanc <= 1.9.0
|
|
|
|
std::ostringstream ss;
|
|
ss << std::setprecision(17) << v;
|
|
return ss.str();
|
|
}
|
|
|
|
|
|
ORTHANC_FORCE_INLINE
|
|
static std::string DoubleToString(double v)
|
|
{
|
|
//return boost::lexical_cast<std::string>(v); // This was used in Orthanc <= 1.9.0
|
|
|
|
std::ostringstream ss;
|
|
ss << std::setprecision(17) << v;
|
|
return ss.str();
|
|
}
|
|
|
|
|
|
#define DCMTK_TO_CTYPE_CONVERTER(converter, cType, dcmtkType, getter, toStringFunction) \
|
|
\
|
|
struct converter \
|
|
{ \
|
|
typedef cType CType; \
|
|
\
|
|
ORTHANC_FORCE_INLINE \
|
|
static bool Apply(CType& result, \
|
|
DcmElement& element, \
|
|
size_t i) \
|
|
{ \
|
|
return dynamic_cast<dcmtkType&>(element).getter(result, i).good(); \
|
|
} \
|
|
\
|
|
ORTHANC_FORCE_INLINE \
|
|
static std::string ToString(CType value) \
|
|
{ \
|
|
return toStringFunction(value); \
|
|
} \
|
|
};
|
|
|
|
DCMTK_TO_CTYPE_CONVERTER(DcmtkToSint32Converter, Sint32, DcmSignedLong, getSint32, boost::lexical_cast<std::string>)
|
|
DCMTK_TO_CTYPE_CONVERTER(DcmtkToSint16Converter, Sint16, DcmSignedShort, getSint16, boost::lexical_cast<std::string>)
|
|
DCMTK_TO_CTYPE_CONVERTER(DcmtkToUint32Converter, Uint32, DcmUnsignedLong, getUint32, boost::lexical_cast<std::string>)
|
|
DCMTK_TO_CTYPE_CONVERTER(DcmtkToUint16Converter, Uint16, DcmUnsignedShort, getUint16, boost::lexical_cast<std::string>)
|
|
DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat32Converter, Float32, DcmFloatingPointSingle, getFloat32, FloatToString)
|
|
DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDouble, getFloat64, DoubleToString)
|
|
|
|
|
|
template <typename F>
|
|
static DicomValue* ApplyDcmtkToCTypeConverter(DcmElement& element)
|
|
{
|
|
F f;
|
|
typename F::CType value;
|
|
|
|
if (element.getLength() > sizeof(typename F::CType)
|
|
&& (element.getLength() % sizeof(typename F::CType)) == 0)
|
|
{
|
|
size_t count = element.getLength() / sizeof(typename F::CType);
|
|
std::vector<std::string> strings;
|
|
for (size_t i = 0; i < count; i++) {
|
|
if (f.Apply(value, element, i)) {
|
|
strings.push_back(F::ToString(value));
|
|
}
|
|
}
|
|
return new DicomValue(boost::algorithm::join(strings, "\\"), false);
|
|
}
|
|
else if (f.Apply(value, element, 0)) {
|
|
return new DicomValue(F::ToString(value), false);
|
|
}
|
|
else {
|
|
return new DicomValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::InitializeDictionary(bool loadPrivateDictionary)
|
|
{
|
|
CLOG(INFO, DICOM) << "Using DCMTK version: " << DCMTK_VERSION_NUMBER;
|
|
|
|
#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
|
|
{
|
|
DictionaryWriterLock lock;
|
|
|
|
lock.GetDictionary().clear();
|
|
|
|
CLOG(INFO, DICOM) << "Loading the embedded dictionaries";
|
|
/**
|
|
* Do not load DICONDE dictionary, it breaks the other tags. The
|
|
* command "strace storescu 2>&1 |grep dic" shows that DICONDE
|
|
* dictionary is not loaded by storescu.
|
|
**/
|
|
//LoadEmbeddedDictionary(lock.GetDictionary(), FrameworkResources::DICTIONARY_DICONDE);
|
|
|
|
LoadEmbeddedDictionary(lock.GetDictionary(), FrameworkResources::DICTIONARY_DICOM);
|
|
|
|
if (loadPrivateDictionary)
|
|
{
|
|
CLOG(INFO, DICOM) << "Loading the embedded dictionary of private tags";
|
|
LoadEmbeddedDictionary(lock.GetDictionary(), FrameworkResources::DICTIONARY_PRIVATE);
|
|
}
|
|
else
|
|
{
|
|
CLOG(INFO, DICOM) << "The dictionary of private tags has not been loaded";
|
|
}
|
|
}
|
|
#else
|
|
{
|
|
std::vector<std::string> dictionaries;
|
|
|
|
const char* env = std::getenv(DCM_DICT_ENVIRONMENT_VARIABLE);
|
|
if (env != NULL)
|
|
{
|
|
// This mimics the behavior of DCMTK:
|
|
// https://support.dcmtk.org/docs/file_envvars.html
|
|
#if defined(_WIN32)
|
|
Toolbox::TokenizeString(dictionaries, std::string(env), ';');
|
|
#else
|
|
Toolbox::TokenizeString(dictionaries, std::string(env), ':');
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
boost::filesystem::path base = DCMTK_DICTIONARY_DIR;
|
|
dictionaries.push_back((base / "dicom.dic").string());
|
|
|
|
if (loadPrivateDictionary)
|
|
{
|
|
dictionaries.push_back((base / "private.dic").string());
|
|
}
|
|
}
|
|
|
|
LoadExternalDictionaries(dictionaries);
|
|
hasExternalDictionaries_ = false; // Fix the side-effect of "LoadExternalDictionaries()"
|
|
}
|
|
#endif
|
|
|
|
/* make sure data dictionary is loaded */
|
|
if (!dcmDataDict.isDictionaryLoaded())
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError,
|
|
"No DICOM dictionary loaded, check environment variable: " +
|
|
std::string(DCM_DICT_ENVIRONMENT_VARIABLE));
|
|
}
|
|
|
|
{
|
|
// Test the dictionary with a simple DICOM tag
|
|
DcmTag key(0x0010, 0x1030); // This is PatientWeight
|
|
if (key.getEVR() != EVR_DS)
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError,
|
|
"The DICOM dictionary has not been correctly read");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::LoadExternalDictionaries(const std::vector<std::string>& dictionaries)
|
|
{
|
|
DictionaryWriterLock lock;
|
|
|
|
CLOG(INFO, DICOM) << "Clearing the DICOM dictionary";
|
|
lock.GetDictionary().clear();
|
|
|
|
for (size_t i = 0; i < dictionaries.size(); i++)
|
|
{
|
|
LOG(WARNING) << "Loading external DICOM dictionary: \"" << dictionaries[i] << "\"";
|
|
|
|
if (!lock.GetDictionary().loadDictionary(dictionaries[i].c_str()))
|
|
{
|
|
throw OrthancException(ErrorCode_InexistentFile);
|
|
}
|
|
}
|
|
|
|
hasExternalDictionaries_ = true;
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::RegisterDictionaryTag(const DicomTag& tag,
|
|
ValueRepresentation vr,
|
|
const std::string& name,
|
|
unsigned int minMultiplicity,
|
|
unsigned int maxMultiplicity,
|
|
const std::string& privateCreator)
|
|
{
|
|
if (minMultiplicity < 1)
|
|
{
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
}
|
|
|
|
bool arbitrary = false;
|
|
if (maxMultiplicity == 0)
|
|
{
|
|
maxMultiplicity = DcmVariableVM;
|
|
arbitrary = true;
|
|
}
|
|
else if (maxMultiplicity < minMultiplicity)
|
|
{
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
}
|
|
|
|
DcmEVR evr = ToDcmtkBridge::Convert(vr);
|
|
|
|
CLOG(INFO, DICOM) << "Registering tag in dictionary: (" << tag.Format() << ") "
|
|
<< (DcmVR(evr).getValidVRName()) << " "
|
|
<< name << " (multiplicity: " << minMultiplicity << "-"
|
|
<< (arbitrary ? "n" : boost::lexical_cast<std::string>(maxMultiplicity)) << ")";
|
|
|
|
std::unique_ptr<DcmDictEntry> entry;
|
|
if (privateCreator.empty())
|
|
{
|
|
if (tag.GetGroup() % 2 == 1)
|
|
{
|
|
char buf[128];
|
|
sprintf(buf, "Warning: You are registering a private tag (%04x,%04x), "
|
|
"but no private creator was associated with it",
|
|
tag.GetGroup(), tag.GetElement());
|
|
LOG(WARNING) << buf;
|
|
}
|
|
|
|
entry.reset(new DcmDictEntry(tag.GetGroup(),
|
|
tag.GetElement(),
|
|
evr, name.c_str(),
|
|
static_cast<int>(minMultiplicity),
|
|
static_cast<int>(maxMultiplicity),
|
|
NULL /* version */,
|
|
OFTrue /* doCopyString */,
|
|
NULL /* private creator */));
|
|
}
|
|
else
|
|
{
|
|
// "Private Data Elements have an odd Group Number that is not
|
|
// (0001,eeee), (0003,eeee), (0005,eeee), (0007,eeee), or
|
|
// (FFFF,eeee)."
|
|
if (tag.GetGroup() % 2 == 0 /* even */ ||
|
|
tag.GetGroup() == 0x0001 ||
|
|
tag.GetGroup() == 0x0003 ||
|
|
tag.GetGroup() == 0x0005 ||
|
|
tag.GetGroup() == 0x0007 ||
|
|
tag.GetGroup() == 0xffff)
|
|
{
|
|
char buf[128];
|
|
sprintf(buf, "Trying to register private tag (%04x,%04x), but it must have an odd group >= 0x0009",
|
|
tag.GetGroup(), tag.GetElement());
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange, std::string(buf));
|
|
}
|
|
|
|
entry.reset(new DcmDictEntry(tag.GetGroup(),
|
|
tag.GetElement(),
|
|
evr, name.c_str(),
|
|
static_cast<int>(minMultiplicity),
|
|
static_cast<int>(maxMultiplicity),
|
|
"private" /* version */,
|
|
OFTrue /* doCopyString */,
|
|
privateCreator.c_str()));
|
|
}
|
|
|
|
entry->setGroupRangeRestriction(DcmDictRange_Unspecified);
|
|
entry->setElementRangeRestriction(DcmDictRange_Unspecified);
|
|
|
|
{
|
|
DictionaryWriterLock lock;
|
|
|
|
if (lock.GetDictionary().findEntry(DcmTagKey(tag.GetGroup(), tag.GetElement()),
|
|
privateCreator.empty() ? NULL : privateCreator.c_str()))
|
|
{
|
|
throw OrthancException(ErrorCode_AlreadyExistingTag,
|
|
"Cannot register twice the tag (" + tag.Format() +
|
|
"), whose symbolic name is \"" + name + "\"");
|
|
}
|
|
else
|
|
{
|
|
lock.GetDictionary().addEntry(entry.release());
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
Encoding FromDcmtkBridge::DetectEncoding(bool& hasCodeExtensions,
|
|
DcmItem& dataset,
|
|
Encoding defaultEncoding)
|
|
{
|
|
// http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2
|
|
|
|
OFString tmp;
|
|
if (dataset.findAndGetOFStringArray(DCM_SpecificCharacterSet, tmp).good())
|
|
{
|
|
std::vector<std::string> tokens;
|
|
Toolbox::TokenizeString(tokens, std::string(tmp.c_str()), '\\');
|
|
|
|
hasCodeExtensions = (tokens.size() > 1);
|
|
|
|
for (size_t i = 0; i < tokens.size(); i++)
|
|
{
|
|
std::string characterSet = Toolbox::StripSpaces(tokens[i]);
|
|
|
|
if (!characterSet.empty())
|
|
{
|
|
Encoding encoding;
|
|
|
|
if (GetDicomEncoding(encoding, characterSet.c_str()))
|
|
{
|
|
// The specific character set is supported by the Orthanc core
|
|
return encoding;
|
|
}
|
|
else
|
|
{
|
|
LOG(WARNING) << "Value of Specific Character Set (0008,0005) is not supported: " << characterSet
|
|
<< ", fallback to ASCII (remove all special characters)";
|
|
return Encoding_Ascii;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
hasCodeExtensions = false;
|
|
}
|
|
|
|
// No specific character set tag: Use the default encoding
|
|
return defaultEncoding;
|
|
}
|
|
|
|
|
|
Encoding FromDcmtkBridge::DetectEncoding(DcmItem &dataset,
|
|
Encoding defaultEncoding)
|
|
{
|
|
bool hasCodeExtensions; // ignored
|
|
return DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::ExtractDicomSummary(DicomMap& target,
|
|
DcmItem& dataset,
|
|
unsigned int maxStringLength,
|
|
const std::set<DicomTag>& ignoreTagLength)
|
|
{
|
|
const Encoding defaultEncoding = GetDefaultDicomEncoding();
|
|
|
|
bool hasCodeExtensions;
|
|
Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
|
|
|
|
target.Clear();
|
|
for (unsigned long i = 0; i < dataset.card(); i++)
|
|
{
|
|
DcmElement* element = dataset.getElement(i);
|
|
if (element && element->isLeaf())
|
|
{
|
|
target.SetValueInternal(element->getTag().getGTag(),
|
|
element->getTag().getETag(),
|
|
ConvertLeafElement(*element, DicomToJsonFlags_Default,
|
|
maxStringLength, encoding, hasCodeExtensions, ignoreTagLength));
|
|
}
|
|
else
|
|
{
|
|
DcmSequenceOfItems* sequence = dynamic_cast<DcmSequenceOfItems*>(element);
|
|
|
|
if (sequence)
|
|
{
|
|
Json::Value jsonSequence = Json::arrayValue;
|
|
for (unsigned long s = 0; s < sequence->card(); s++)
|
|
{
|
|
DcmItem* child = sequence->getItem(s);
|
|
Json::Value& v = jsonSequence.append(Json::objectValue);
|
|
DatasetToJson(v, *child, DicomToJsonFormat_Full, DicomToJsonFlags_Default,
|
|
maxStringLength, encoding, hasCodeExtensions,
|
|
ignoreTagLength, 1);
|
|
}
|
|
|
|
target.SetSequenceValue(DicomTag(element->getTag().getGTag(), element->getTag().getETag()),
|
|
jsonSequence);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
DicomTag FromDcmtkBridge::Convert(const DcmTag& tag)
|
|
{
|
|
return DicomTag(tag.getGTag(), tag.getETag());
|
|
}
|
|
|
|
|
|
DicomTag FromDcmtkBridge::GetTag(const DcmElement& element)
|
|
{
|
|
return DicomTag(element.getGTag(), element.getETag());
|
|
}
|
|
|
|
|
|
static DicomValue* CreateValueFromUtf8String(const DicomTag& tag,
|
|
const std::string& utf8,
|
|
unsigned int maxStringLength,
|
|
const std::set<DicomTag>& ignoreTagLength)
|
|
{
|
|
if (maxStringLength != 0 &&
|
|
utf8.size() > maxStringLength &&
|
|
ignoreTagLength.find(tag) == ignoreTagLength.end())
|
|
{
|
|
return new DicomValue; // Too long, create a NULL value
|
|
}
|
|
else
|
|
{
|
|
return new DicomValue(utf8, false);
|
|
}
|
|
}
|
|
|
|
|
|
DicomValue* FromDcmtkBridge::ConvertLeafElement(DcmElement& element,
|
|
DicomToJsonFlags flags,
|
|
unsigned int maxStringLength,
|
|
Encoding encoding,
|
|
bool hasCodeExtensions,
|
|
const std::set<DicomTag>& ignoreTagLength)
|
|
{
|
|
if (!element.isLeaf())
|
|
{
|
|
// This function is only applicable to leaf elements
|
|
throw OrthancException(ErrorCode_BadParameterType);
|
|
}
|
|
|
|
{
|
|
char *c = NULL;
|
|
if (element.isaString() &&
|
|
element.getString(c).good())
|
|
{
|
|
if (c == NULL) // This case corresponds to the empty string
|
|
{
|
|
return new DicomValue("", false);
|
|
}
|
|
else
|
|
{
|
|
const std::string s(c);
|
|
const std::string utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions);
|
|
return CreateValueFromUtf8String(GetTag(element), utf8, maxStringLength, ignoreTagLength);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (element.getVR() == EVR_UN)
|
|
{
|
|
/**
|
|
* Unknown value representation: Lookup in the dictionary. This
|
|
* is notably the case for private tags registered with the
|
|
* "Dictionary" configuration option, or for public tags with
|
|
* "EVR_UN" in the case of Little Endian Implicit transfer
|
|
* syntax (cf. DICOM CP 246).
|
|
* ftp://medical.nema.org/medical/dicom/final/cp246_ft.pdf
|
|
**/
|
|
DictionaryReaderLock lock;
|
|
|
|
// The "entry" value is only valid while "lock" is active
|
|
const DcmDictEntry* entry = lock.GetDictionary().findEntry(element.getTag().getXTag(),
|
|
element.getTag().getPrivateCreator());
|
|
if (entry != NULL &&
|
|
entry->getVR().isaString())
|
|
{
|
|
Uint8* data = NULL;
|
|
|
|
if (element.getUint8Array(data) == EC_Normal)
|
|
{
|
|
Uint32 length = element.getLength();
|
|
|
|
if (data == NULL ||
|
|
length == 0)
|
|
{
|
|
return new DicomValue("", false); // Empty string
|
|
}
|
|
|
|
// Remove the trailing padding, if any
|
|
if (length > 0 &&
|
|
length % 2 == 0 &&
|
|
data[length - 1] == '\0')
|
|
{
|
|
length = length - 1;
|
|
}
|
|
|
|
if (element.getTag().isPrivate())
|
|
{
|
|
// For private tags, we do not try and convert to UTF-8,
|
|
// as nothing ensures that the encoding of the private tag
|
|
// is the same as that of the remaining of the DICOM
|
|
// dataset. Only go for ASCII strings.
|
|
if (Toolbox::IsAsciiString(data, length))
|
|
{
|
|
const std::string s(reinterpret_cast<const char*>(data), length);
|
|
return CreateValueFromUtf8String(GetTag(element), s, maxStringLength, ignoreTagLength);
|
|
}
|
|
else
|
|
{
|
|
// Not a plain ASCII string: Consider it as a binary
|
|
// value that is handled in the switch-case below
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// For public tags, convert to UTF-8 by using the
|
|
// "SpecificCharacterSet" tag, if present. This branch is
|
|
// new in Orthanc 1.9.1 (cf. DICOM CP 246).
|
|
const std::string s(reinterpret_cast<const char*>(data), length);
|
|
const std::string utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions);
|
|
return CreateValueFromUtf8String(GetTag(element), utf8, maxStringLength, ignoreTagLength);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
try
|
|
{
|
|
// http://support.dcmtk.org/docs/dcvr_8h-source.html
|
|
switch (element.getVR())
|
|
{
|
|
|
|
/**
|
|
* Deal with binary data (including PixelData).
|
|
**/
|
|
|
|
case EVR_OB: // other byte
|
|
case EVR_OF: // other float
|
|
case EVR_OW: // other word
|
|
case EVR_UN: // unknown value representation
|
|
case EVR_ox: // OB or OW depending on context
|
|
case EVR_DS: // decimal string
|
|
case EVR_IS: // integer string
|
|
case EVR_AS: // age string
|
|
case EVR_DA: // date string
|
|
case EVR_DT: // date time string
|
|
case EVR_TM: // time string
|
|
case EVR_AE: // application entity title
|
|
case EVR_CS: // code string
|
|
case EVR_SH: // short string
|
|
case EVR_LO: // long string
|
|
case EVR_ST: // short text
|
|
case EVR_LT: // long text
|
|
case EVR_UT: // unlimited text
|
|
case EVR_PN: // person name
|
|
case EVR_UI: // unique identifier
|
|
case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
|
|
case EVR_UNKNOWN2B: // used internally for elements with unknown VR with 2-byte length field in explicit VR
|
|
{
|
|
if (!(flags & DicomToJsonFlags_ConvertBinaryToNull))
|
|
{
|
|
Uint8* data = NULL;
|
|
Uint16* data16 = NULL;
|
|
if (element.getUint8Array(data) == EC_Normal)
|
|
{
|
|
return new DicomValue(reinterpret_cast<const char*>(data), element.getLength(), true);
|
|
}
|
|
else if (element.getUint16Array(data16) == EC_Normal)
|
|
{
|
|
return new DicomValue(reinterpret_cast<const char*>(data16), element.getLength(), true);
|
|
}
|
|
}
|
|
|
|
return new DicomValue;
|
|
}
|
|
|
|
/**
|
|
* Numeric types
|
|
**/
|
|
|
|
case EVR_SL: // signed long
|
|
{
|
|
return ApplyDcmtkToCTypeConverter<DcmtkToSint32Converter>(element);
|
|
}
|
|
|
|
case EVR_SS: // signed short
|
|
{
|
|
return ApplyDcmtkToCTypeConverter<DcmtkToSint16Converter>(element);
|
|
}
|
|
|
|
case EVR_UL: // unsigned long
|
|
{
|
|
return ApplyDcmtkToCTypeConverter<DcmtkToUint32Converter>(element);
|
|
}
|
|
|
|
case EVR_US: // unsigned short
|
|
{
|
|
return ApplyDcmtkToCTypeConverter<DcmtkToUint16Converter>(element);
|
|
}
|
|
|
|
case EVR_FL: // float single-precision
|
|
{
|
|
return ApplyDcmtkToCTypeConverter<DcmtkToFloat32Converter>(element);
|
|
}
|
|
|
|
case EVR_FD: // float double-precision
|
|
{
|
|
return ApplyDcmtkToCTypeConverter<DcmtkToFloat64Converter>(element);
|
|
}
|
|
|
|
|
|
/**
|
|
* Attribute tag.
|
|
**/
|
|
|
|
case EVR_AT:
|
|
{
|
|
DcmTagKey tag;
|
|
if (dynamic_cast<DcmAttributeTag&>(element).getTagVal(tag, 0).good())
|
|
{
|
|
DicomTag t(tag.getGroup(), tag.getElement());
|
|
return new DicomValue(t.Format(), false);
|
|
}
|
|
else
|
|
{
|
|
return new DicomValue;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Sequence types, should never occur at this point because of
|
|
* "element.isLeaf()".
|
|
**/
|
|
|
|
case EVR_SQ: // sequence of items
|
|
return new DicomValue;
|
|
|
|
|
|
/**
|
|
* Internal to DCMTK.
|
|
**/
|
|
|
|
case EVR_xs: // SS or US depending on context
|
|
case EVR_lt: // US, SS or OW depending on context, used for LUT Data (thus the name)
|
|
case EVR_na: // na="not applicable", for data which has no VR
|
|
case EVR_up: // up="unsigned pointer", used internally for DICOMDIR suppor
|
|
case EVR_item: // used internally for items
|
|
case EVR_metainfo: // used internally for meta info datasets
|
|
case EVR_dataset: // used internally for datasets
|
|
case EVR_fileFormat: // used internally for DICOM files
|
|
case EVR_dicomDir: // used internally for DICOMDIR objects
|
|
case EVR_dirRecord: // used internally for DICOMDIR records
|
|
case EVR_pixelSQ: // used internally for pixel sequences in a compressed image
|
|
case EVR_pixelItem: // used internally for pixel items in a compressed image
|
|
case EVR_PixelData: // used internally for uncompressed pixeld data
|
|
case EVR_OverlayData: // used internally for overlay data
|
|
return new DicomValue;
|
|
|
|
|
|
/**
|
|
* Default case.
|
|
**/
|
|
|
|
default:
|
|
return new DicomValue;
|
|
}
|
|
}
|
|
catch (boost::bad_lexical_cast&)
|
|
{
|
|
return new DicomValue;
|
|
}
|
|
catch (std::bad_cast&)
|
|
{
|
|
return new DicomValue;
|
|
}
|
|
}
|
|
|
|
|
|
static Json::Value& PrepareNode(Json::Value& parent,
|
|
DcmElement& element,
|
|
DicomToJsonFormat format)
|
|
{
|
|
assert(parent.type() == Json::objectValue);
|
|
|
|
DicomTag tag(FromDcmtkBridge::GetTag(element));
|
|
const std::string formattedTag = tag.Format();
|
|
|
|
if (format == DicomToJsonFormat_Short)
|
|
{
|
|
parent[formattedTag] = Json::nullValue;
|
|
return parent[formattedTag];
|
|
}
|
|
|
|
// This code gives access to the name of the private tags
|
|
std::string tagName = FromDcmtkBridge::GetTagName(element);
|
|
|
|
switch (format)
|
|
{
|
|
case DicomToJsonFormat_Human:
|
|
parent[tagName] = Json::nullValue;
|
|
return parent[tagName];
|
|
|
|
case DicomToJsonFormat_Full:
|
|
{
|
|
parent[formattedTag] = Json::objectValue;
|
|
Json::Value& node = parent[formattedTag];
|
|
|
|
if (element.isLeaf())
|
|
{
|
|
node["Name"] = tagName;
|
|
|
|
if (element.getTag().getPrivateCreator() != NULL)
|
|
{
|
|
node["PrivateCreator"] = element.getTag().getPrivateCreator();
|
|
}
|
|
|
|
return node;
|
|
}
|
|
else
|
|
{
|
|
node["Name"] = tagName;
|
|
node["Type"] = "Sequence";
|
|
node["Value"] = Json::nullValue;
|
|
return node["Value"];
|
|
}
|
|
}
|
|
|
|
default:
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
}
|
|
}
|
|
|
|
|
|
static void LeafValueToJson(Json::Value& target,
|
|
const DicomValue& value,
|
|
DicomToJsonFormat format,
|
|
DicomToJsonFlags flags,
|
|
unsigned int maxStringLength)
|
|
{
|
|
Json::Value* targetValue = NULL;
|
|
Json::Value* targetType = NULL;
|
|
|
|
switch (format)
|
|
{
|
|
case DicomToJsonFormat_Short:
|
|
case DicomToJsonFormat_Human:
|
|
{
|
|
assert(target.type() == Json::nullValue);
|
|
targetValue = ⌖
|
|
break;
|
|
}
|
|
|
|
case DicomToJsonFormat_Full:
|
|
{
|
|
assert(target.type() == Json::objectValue);
|
|
target["Value"] = Json::nullValue;
|
|
target["Type"] = Json::nullValue;
|
|
targetType = &target["Type"];
|
|
targetValue = &target["Value"];
|
|
break;
|
|
}
|
|
|
|
default:
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
}
|
|
|
|
assert(targetValue != NULL);
|
|
assert(targetValue->type() == Json::nullValue);
|
|
assert(targetType == NULL || targetType->type() == Json::nullValue);
|
|
|
|
if (value.IsNull())
|
|
{
|
|
if (targetType != NULL)
|
|
{
|
|
*targetType = "Null";
|
|
}
|
|
}
|
|
else if (value.IsBinary())
|
|
{
|
|
if (flags & DicomToJsonFlags_ConvertBinaryToAscii)
|
|
{
|
|
*targetValue = Toolbox::ConvertToAscii(value.GetContent());
|
|
}
|
|
else
|
|
{
|
|
std::string s;
|
|
value.FormatDataUriScheme(s);
|
|
*targetValue = s;
|
|
}
|
|
|
|
if (targetType != NULL)
|
|
{
|
|
*targetType = "Binary";
|
|
}
|
|
}
|
|
else if (maxStringLength == 0 ||
|
|
value.GetContent().size() <= maxStringLength)
|
|
{
|
|
*targetValue = value.GetContent();
|
|
|
|
if (targetType != NULL)
|
|
{
|
|
*targetType = "String";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (targetType != NULL)
|
|
{
|
|
*targetType = "TooLong";
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::ElementToJson(Json::Value& parent,
|
|
DcmElement& element,
|
|
DicomToJsonFormat format,
|
|
DicomToJsonFlags flags,
|
|
unsigned int maxStringLength,
|
|
Encoding encoding,
|
|
bool hasCodeExtensions,
|
|
const std::set<DicomTag>& ignoreTagLength,
|
|
unsigned int depth)
|
|
{
|
|
if (parent.type() == Json::nullValue)
|
|
{
|
|
parent = Json::objectValue;
|
|
}
|
|
|
|
assert(parent.type() == Json::objectValue);
|
|
Json::Value& target = PrepareNode(parent, element, format);
|
|
|
|
if (element.isLeaf())
|
|
{
|
|
// The "0" below lets "LeafValueToJson()" take care of "TooLong" values
|
|
std::unique_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement
|
|
(element, flags, 0, encoding, hasCodeExtensions, ignoreTagLength));
|
|
|
|
if (ignoreTagLength.find(GetTag(element)) == ignoreTagLength.end())
|
|
{
|
|
LeafValueToJson(target, *v, format, flags, maxStringLength);
|
|
}
|
|
else
|
|
{
|
|
LeafValueToJson(target, *v, format, flags, 0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
assert(target.type() == Json::nullValue);
|
|
target = Json::arrayValue;
|
|
|
|
// "All subclasses of DcmElement except for DcmSequenceOfItems
|
|
// are leaf nodes, while DcmSequenceOfItems, DcmItem, DcmDataset
|
|
// etc. are not." The following dynamic_cast is thus OK.
|
|
DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(element);
|
|
|
|
for (unsigned long i = 0; i < sequence.card(); i++)
|
|
{
|
|
DcmItem* child = sequence.getItem(i);
|
|
Json::Value& v = target.append(Json::objectValue);
|
|
DatasetToJson(v, *child, format, flags, maxStringLength, encoding, hasCodeExtensions,
|
|
ignoreTagLength, depth + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::DatasetToJson(Json::Value& parent,
|
|
DcmItem& item,
|
|
DicomToJsonFormat format,
|
|
DicomToJsonFlags flags,
|
|
unsigned int maxStringLength,
|
|
Encoding encoding,
|
|
bool hasCodeExtensions,
|
|
const std::set<DicomTag>& ignoreTagLength,
|
|
unsigned int depth)
|
|
{
|
|
assert(parent.type() == Json::objectValue);
|
|
|
|
for (unsigned long i = 0; i < item.card(); i++)
|
|
{
|
|
DcmElement* element = item.getElement(i);
|
|
if (element == NULL)
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
|
|
DicomTag tag(FromDcmtkBridge::Convert(element->getTag()));
|
|
|
|
// New flag in Orthanc 1.9.1
|
|
if (depth == 0 &&
|
|
(flags & DicomToJsonFlags_StopAfterPixelData) &&
|
|
tag > DICOM_TAG_PIXEL_DATA)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// New flag in Orthanc 1.9.1
|
|
if ((flags & DicomToJsonFlags_SkipGroupLengths) &&
|
|
tag.GetElement() == 0x0000)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
/*element->getTag().isPrivate()*/
|
|
if (tag.IsPrivate() &&
|
|
!(flags & DicomToJsonFlags_IncludePrivateTags))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!(flags & DicomToJsonFlags_IncludeUnknownTags))
|
|
{
|
|
DictionaryReaderLock lock;
|
|
if (lock.GetDictionary().findEntry(element->getTag(), element->getTag().getPrivateCreator()) == NULL)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (IsBinaryTag(element->getTag()))
|
|
{
|
|
// This is a binary tag
|
|
if ((tag == DICOM_TAG_PIXEL_DATA && !(flags & DicomToJsonFlags_IncludePixelData)) ||
|
|
(tag != DICOM_TAG_PIXEL_DATA && !(flags & DicomToJsonFlags_IncludeBinary)))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
FromDcmtkBridge::ElementToJson(parent, *element, format, flags, maxStringLength, encoding,
|
|
hasCodeExtensions, ignoreTagLength, depth);
|
|
}
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::ExtractDicomAsJson(Json::Value& target,
|
|
DcmDataset& dataset,
|
|
DicomToJsonFormat format,
|
|
DicomToJsonFlags flags,
|
|
unsigned int maxStringLength,
|
|
const std::set<DicomTag>& ignoreTagLength)
|
|
{
|
|
const Encoding defaultEncoding = GetDefaultDicomEncoding();
|
|
|
|
bool hasCodeExtensions;
|
|
Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
|
|
|
|
target = Json::objectValue;
|
|
DatasetToJson(target, dataset, format, flags, maxStringLength, encoding, hasCodeExtensions, ignoreTagLength, 0);
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::ExtractHeaderAsJson(Json::Value& target,
|
|
DcmMetaInfo& dataset,
|
|
DicomToJsonFormat format,
|
|
DicomToJsonFlags flags,
|
|
unsigned int maxStringLength)
|
|
{
|
|
std::set<DicomTag> ignoreTagLength;
|
|
target = Json::objectValue;
|
|
DatasetToJson(target, dataset, format, flags, maxStringLength, Encoding_Ascii, false, ignoreTagLength, 0);
|
|
}
|
|
|
|
|
|
static std::string GetTagNameInternal(DcmTag& tag)
|
|
{
|
|
if (!hasExternalDictionaries_)
|
|
{
|
|
/**
|
|
* Some patches for important tags because of different DICOM
|
|
* dictionaries between DCMTK versions. Since Orthanc 1.9.4, we
|
|
* don't apply these patches if external dictionaries are
|
|
* loaded, notably for compatibility with DICONDE. In Orthanc <=
|
|
* 1.9.3, this was done by method "DicomTag::GetMainTagsName()".
|
|
**/
|
|
|
|
DicomTag tmp(tag.getGroup(), tag.getElement());
|
|
|
|
if (tmp == DICOM_TAG_ACCESSION_NUMBER)
|
|
return "AccessionNumber";
|
|
|
|
if (tmp == DICOM_TAG_SOP_INSTANCE_UID)
|
|
return "SOPInstanceUID";
|
|
|
|
if (tmp == DICOM_TAG_PATIENT_ID)
|
|
return "PatientID";
|
|
|
|
if (tmp == DICOM_TAG_SERIES_INSTANCE_UID)
|
|
return "SeriesInstanceUID";
|
|
|
|
if (tmp == DICOM_TAG_STUDY_INSTANCE_UID)
|
|
return "StudyInstanceUID";
|
|
|
|
if (tmp == DICOM_TAG_PIXEL_DATA)
|
|
return "PixelData";
|
|
|
|
if (tmp == DICOM_TAG_IMAGE_INDEX)
|
|
return "ImageIndex";
|
|
|
|
if (tmp == DICOM_TAG_INSTANCE_NUMBER)
|
|
return "InstanceNumber";
|
|
|
|
if (tmp == DICOM_TAG_NUMBER_OF_SLICES)
|
|
return "NumberOfSlices";
|
|
|
|
if (tmp == DICOM_TAG_NUMBER_OF_FRAMES)
|
|
return "NumberOfFrames";
|
|
|
|
if (tmp == DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)
|
|
return "CardiacNumberOfImages";
|
|
|
|
if (tmp == DICOM_TAG_IMAGES_IN_ACQUISITION)
|
|
return "ImagesInAcquisition";
|
|
|
|
if (tmp == DICOM_TAG_PATIENT_NAME)
|
|
return "PatientName";
|
|
|
|
if (tmp == DICOM_TAG_IMAGE_POSITION_PATIENT)
|
|
return "ImagePositionPatient";
|
|
|
|
if (tmp == DICOM_TAG_IMAGE_ORIENTATION_PATIENT)
|
|
return "ImageOrientationPatient";
|
|
|
|
// New in Orthanc 1.6.0, as tagged as "RETIRED_" since DCMTK 3.6.4
|
|
if (tmp == DICOM_TAG_OTHER_PATIENT_IDS)
|
|
return "OtherPatientIDs";
|
|
|
|
// End of patches
|
|
}
|
|
|
|
#if 0
|
|
// This version explicitly calls the dictionary
|
|
const DcmDataDictionary& dict = dcmDataDict.rdlock();
|
|
const DcmDictEntry* entry = dict.findEntry(tag, NULL);
|
|
|
|
std::string s(DcmTag_ERROR_TagName);
|
|
if (entry != NULL)
|
|
{
|
|
s = std::string(entry->getTagName());
|
|
}
|
|
|
|
dcmDataDict.unlock();
|
|
return s;
|
|
#else
|
|
const char* name = tag.getTagName();
|
|
if (name == NULL)
|
|
{
|
|
return DcmTag_ERROR_TagName;
|
|
}
|
|
else
|
|
{
|
|
return std::string(name);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
|
|
static bool GetTagFromNameInternal(DicomTag& tag, const std::string& tagName)
|
|
{
|
|
// conversion from old tag names (ex: RETIRED_OtherPatientIDs is the new name for OtherPatientIDs that is still a valid name for DICOM_TAG_OTHER_PATIENT_IDS)
|
|
if (tagName == "OtherPatientIDs")
|
|
{
|
|
tag = DICOM_TAG_OTHER_PATIENT_IDS;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
std::string FromDcmtkBridge::GetTagName(const DicomTag& t,
|
|
const std::string& privateCreator)
|
|
{
|
|
|
|
DcmTag tag(t.GetGroup(), t.GetElement());
|
|
|
|
if (!privateCreator.empty())
|
|
{
|
|
tag.setPrivateCreator(privateCreator.c_str());
|
|
}
|
|
|
|
return GetTagNameInternal(tag);
|
|
}
|
|
|
|
|
|
std::string FromDcmtkBridge::GetTagName(const DcmElement& element)
|
|
{
|
|
// Copy the tag to ensure const-correctness of DcmElement. Note
|
|
// that the private creator information is also copied.
|
|
DcmTag tag(element.getTag());
|
|
|
|
return GetTagNameInternal(tag);
|
|
}
|
|
|
|
std::string FromDcmtkBridge::GetTagName(const DicomElement &element)
|
|
{
|
|
return GetTagName(element.GetTag(), "");
|
|
}
|
|
|
|
|
|
|
|
DicomTag FromDcmtkBridge::ParseTag(const char* name)
|
|
{
|
|
DicomTag parsed(0, 0);
|
|
if (DicomTag::ParseHexadecimal(parsed, name))
|
|
{
|
|
return parsed;
|
|
}
|
|
|
|
#if 0
|
|
const DcmDataDictionary& dict = dcmDataDict.rdlock();
|
|
const DcmDictEntry* entry = dict.findEntry(name);
|
|
|
|
if (entry == NULL)
|
|
{
|
|
dcmDataDict.unlock();
|
|
throw OrthancException(ErrorCode_UnknownDicomTag);
|
|
}
|
|
else
|
|
{
|
|
DcmTagKey key = entry->getKey();
|
|
DicomTag tag(key.getGroup(), key.getElement());
|
|
dcmDataDict.unlock();
|
|
return tag;
|
|
}
|
|
#else
|
|
DcmTag tag;
|
|
if (DcmTag::findTagFromName(name, tag).good())
|
|
{
|
|
return DicomTag(tag.getGTag(), tag.getETag());
|
|
}
|
|
else
|
|
{
|
|
DicomTag dcmTag(0, 0);
|
|
if (GetTagFromNameInternal(dcmTag, name))
|
|
{
|
|
return dcmTag;
|
|
}
|
|
|
|
CLOG(INFO, DICOM) << "Unknown DICOM tag: \"" << name << "\"";
|
|
throw OrthancException(ErrorCode_UnknownDicomTag, name, false);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
DicomTag FromDcmtkBridge::ParseTag(const std::string &name)
|
|
{
|
|
return ParseTag(name.c_str());
|
|
}
|
|
|
|
bool FromDcmtkBridge::HasTag(const DicomMap &fields, const std::string &tagName)
|
|
{
|
|
return fields.HasTag(ParseTag(tagName));
|
|
}
|
|
|
|
void FromDcmtkBridge::FormatListOfTags(std::string& output, const std::set<DicomTag>& tags)
|
|
{
|
|
std::set<std::string> values;
|
|
for (std::set<DicomTag>::const_iterator it = tags.begin();
|
|
it != tags.end(); ++it)
|
|
{
|
|
values.insert(it->Format());
|
|
}
|
|
|
|
Toolbox::JoinStrings(output, values, ";");
|
|
}
|
|
|
|
void FromDcmtkBridge::FormatListOfTags(Json::Value& output, const std::set<DicomTag>& tags)
|
|
{
|
|
output = Json::arrayValue;
|
|
for (std::set<DicomTag>::const_iterator it = tags.begin();
|
|
it != tags.end(); ++it)
|
|
{
|
|
output.append(it->Format());
|
|
}
|
|
}
|
|
|
|
// parses a list like "0010,0010;PatientBirthDate;0020,0020"
|
|
void FromDcmtkBridge::ParseListOfTags(std::set<DicomTag>& result, const std::string& source)
|
|
{
|
|
result.clear();
|
|
|
|
std::vector<std::string> tokens;
|
|
Toolbox::TokenizeString(tokens, source, ';');
|
|
|
|
for (std::vector<std::string>::const_iterator it = tokens.begin();
|
|
it != tokens.end(); ++it)
|
|
{
|
|
if (it->size() > 0)
|
|
{
|
|
DicomTag tag = FromDcmtkBridge::ParseTag(*it);
|
|
result.insert(tag);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::ParseListOfTags(std::set<DicomTag>& result, const Json::Value& source)
|
|
{
|
|
result.clear();
|
|
|
|
if (!source.isArray())
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest, "List of tags is not an array");
|
|
}
|
|
|
|
for (Json::ArrayIndex i = 0; i < source.size(); i++)
|
|
{
|
|
const std::string& value = source[i].asString();
|
|
DicomTag tag = FromDcmtkBridge::ParseTag(value);
|
|
result.insert(tag);
|
|
}
|
|
}
|
|
|
|
const DicomValue &FromDcmtkBridge::GetValue(const DicomMap &fields,
|
|
const std::string &tagName)
|
|
{
|
|
return fields.GetValue(ParseTag(tagName));
|
|
}
|
|
|
|
void FromDcmtkBridge::SetValue(DicomMap &target,
|
|
const std::string &tagName,
|
|
DicomValue *value)
|
|
{
|
|
const DicomTag tag = ParseTag(tagName);
|
|
target.SetValueInternal(tag.GetGroup(), tag.GetElement(), value);
|
|
}
|
|
|
|
|
|
bool FromDcmtkBridge::IsUnknownTag(const DicomTag& tag)
|
|
{
|
|
DcmTag tmp(tag.GetGroup(), tag.GetElement());
|
|
return tmp.isUnknownVR();
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::ToJson(Json::Value& result,
|
|
const DicomMap& values,
|
|
DicomToJsonFormat format)
|
|
{
|
|
if (result.type() != Json::objectValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadParameterType);
|
|
}
|
|
|
|
result.clear();
|
|
|
|
for (DicomMap::Content::const_iterator
|
|
it = values.content_.begin(); it != values.content_.end(); ++it)
|
|
{
|
|
switch (format)
|
|
{
|
|
case DicomToJsonFormat_Human:
|
|
{
|
|
// TODO Inject PrivateCreator if some is available in the DicomMap?
|
|
const std::string tagName = GetTagName(it->first, "");
|
|
|
|
if (it->second->IsNull())
|
|
{
|
|
result[tagName] = Json::nullValue;
|
|
}
|
|
else if (it->second->IsSequence())
|
|
{
|
|
result[tagName] = Json::arrayValue;
|
|
const Json::Value& jsonSequence = it->second->GetSequenceContent();
|
|
|
|
for (Json::Value::ArrayIndex i = 0; i < jsonSequence.size(); ++i)
|
|
{
|
|
Json::Value target = Json::objectValue;
|
|
Toolbox::SimplifyDicomAsJson(target, jsonSequence[i], DicomToJsonFormat_Human);
|
|
result[tagName].append(target);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// TODO IsBinary
|
|
result[tagName] = it->second->GetContent();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case DicomToJsonFormat_Full:
|
|
{
|
|
// TODO Inject PrivateCreator if some is available in the DicomMap?
|
|
const std::string tagName = GetTagName(it->first, "");
|
|
|
|
Json::Value value = Json::objectValue;
|
|
|
|
value["Name"] = tagName;
|
|
|
|
if (it->second->IsNull())
|
|
{
|
|
value["Type"] = "Null";
|
|
value["Value"] = Json::nullValue;
|
|
}
|
|
else if (it->second->IsSequence())
|
|
{
|
|
value["Type"] = "Sequence";
|
|
value["Value"] = it->second->GetSequenceContent();
|
|
}
|
|
else
|
|
{
|
|
// TODO IsBinary
|
|
value["Type"] = "String";
|
|
value["Value"] = it->second->GetContent();
|
|
}
|
|
|
|
result[it->first.Format()] = value;
|
|
break;
|
|
}
|
|
|
|
case DicomToJsonFormat_Short:
|
|
{
|
|
const std::string hex = it->first.Format();
|
|
|
|
if (it->second->IsNull())
|
|
{
|
|
result[hex] = Json::nullValue;
|
|
}
|
|
else if (it->second->IsSequence())
|
|
{
|
|
result[hex] = Json::arrayValue;
|
|
const Json::Value& jsonSequence = it->second->GetSequenceContent();
|
|
|
|
for (Json::Value::ArrayIndex i = 0; i < jsonSequence.size(); ++i)
|
|
{
|
|
Json::Value target = Json::objectValue;
|
|
Toolbox::SimplifyDicomAsJson(target, jsonSequence[i], DicomToJsonFormat_Short);
|
|
result[hex].append(target);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// TODO IsBinary
|
|
result[hex] = it->second->GetContent();
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
std::string FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType level)
|
|
{
|
|
char uid[100];
|
|
|
|
switch (level)
|
|
{
|
|
case ResourceType_Patient:
|
|
// The "PatientID" field is of type LO (Long String), 64
|
|
// Bytes Maximum. An UUID is of length 36, thus it can be used
|
|
// as a random PatientID.
|
|
return Toolbox::GenerateUuid();
|
|
|
|
case ResourceType_Instance:
|
|
return dcmGenerateUniqueIdentifier(uid, SITE_INSTANCE_UID_ROOT);
|
|
|
|
case ResourceType_Series:
|
|
return dcmGenerateUniqueIdentifier(uid, SITE_SERIES_UID_ROOT);
|
|
|
|
case ResourceType_Study:
|
|
return dcmGenerateUniqueIdentifier(uid, SITE_STUDY_UID_ROOT);
|
|
|
|
default:
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
}
|
|
}
|
|
|
|
|
|
#if 0
|
|
/**
|
|
* This was the implementation in Orthanc <= 1.12.7. This version
|
|
* uses "DcmFileFormat::calcElementLength()", which cannot handle
|
|
* DICOM files whose size cannot be represented on 32 bits.
|
|
**/
|
|
static bool SaveToMemoryBufferInternal(std::string& buffer,
|
|
DcmFileFormat& dicom,
|
|
E_TransferSyntax xfer,
|
|
std::string& errorMessage)
|
|
{
|
|
E_EncodingType encodingType = /*opt_sequenceType*/ EET_ExplicitLength;
|
|
|
|
// Create a memory buffer with the proper size
|
|
{
|
|
const uint32_t estimatedSize = dicom.calcElementLength(xfer, encodingType); // (*)
|
|
buffer.resize(estimatedSize);
|
|
}
|
|
|
|
DcmOutputBufferStream ob(&buffer[0], buffer.size());
|
|
|
|
// Fill the memory buffer with the meta-header and the dataset
|
|
dicom.transferInit();
|
|
OFCondition c = dicom.write(ob, xfer, encodingType, NULL,
|
|
/*opt_groupLength*/ EGL_recalcGL,
|
|
/*opt_paddingType*/ EPD_noChange,
|
|
/*padlen*/ 0, /*subPadlen*/ 0, /*instanceLength*/ 0,
|
|
EWM_updateMeta /* creates new SOP instance UID on lossy */);
|
|
dicom.transferEnd();
|
|
|
|
if (c.good())
|
|
{
|
|
// The DICOM file is successfully written, truncate the target
|
|
// buffer if its size was overestimated by (*)
|
|
ob.flush();
|
|
|
|
size_t effectiveSize = static_cast<size_t>(ob.filled());
|
|
if (effectiveSize < buffer.size())
|
|
{
|
|
buffer.resize(effectiveSize);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// Error
|
|
buffer.clear();
|
|
errorMessage = std::string(c.text());
|
|
return false;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
|
|
#if 1
|
|
/**
|
|
* This is the cleaner implementation used in Orthanc >= 1.12.8,
|
|
* which allows to write DICOM files larger than 4GB.
|
|
**/
|
|
static bool SaveToMemoryBufferInternal(std::string& buffer,
|
|
DcmFileFormat& dicom,
|
|
E_TransferSyntax xfer,
|
|
std::string& errorMessage)
|
|
{
|
|
ChunkedBufferStream ob;
|
|
|
|
// Fill the (chunked) memory buffer with the meta-header and the dataset
|
|
dicom.transferInit();
|
|
OFCondition c = dicom.write(ob, xfer, /*opt_sequenceType*/ EET_ExplicitLength, NULL,
|
|
/*opt_groupLength*/ EGL_recalcGL,
|
|
/*opt_paddingType*/ EPD_noChange,
|
|
/*padlen*/ 0, /*subPadlen*/ 0, /*instanceLength*/ 0,
|
|
EWM_updateMeta /* creates new SOP instance UID on lossy */);
|
|
dicom.transferEnd();
|
|
|
|
if (c.good())
|
|
{
|
|
ob.flush();
|
|
ob.Flatten(buffer);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// Error
|
|
buffer.clear();
|
|
errorMessage = std::string(c.text());
|
|
return false;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
|
|
bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
|
|
DcmDataset& dataSet)
|
|
{
|
|
std::string errorMessageNotUsed;
|
|
return SaveToMemoryBuffer(buffer, dataSet, errorMessageNotUsed);
|
|
}
|
|
|
|
|
|
|
|
bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
|
|
DcmDataset& dataSet,
|
|
std::string& errorMessage)
|
|
{
|
|
// Determine the transfer syntax which shall be used to write the
|
|
// information to the file. If not possible, switch to the Little
|
|
// Endian syntax, with explicit length.
|
|
|
|
// http://support.dcmtk.org/docs/dcxfer_8h-source.html
|
|
|
|
|
|
/**
|
|
* Note that up to Orthanc 0.7.1 (inclusive), the
|
|
* "EXS_LittleEndianExplicit" was always used to save the DICOM
|
|
* dataset into memory. We now keep the original transfer syntax
|
|
* (if available).
|
|
**/
|
|
E_TransferSyntax xfer = dataSet.getCurrentXfer();
|
|
if (xfer == EXS_Unknown)
|
|
{
|
|
// No information about the original transfer syntax: This is
|
|
// most probably a DICOM dataset that was read from memory.
|
|
xfer = EXS_LittleEndianExplicit;
|
|
}
|
|
|
|
// Create the meta-header information
|
|
DcmFileFormat ff(&dataSet);
|
|
ff.validateMetaInfo(xfer);
|
|
ff.removeInvalidGroups();
|
|
|
|
return SaveToMemoryBufferInternal(buffer, ff, xfer, errorMessage);
|
|
}
|
|
|
|
|
|
bool FromDcmtkBridge::Transcode(DcmFileFormat& dicom,
|
|
DicomTransferSyntax syntax,
|
|
const DcmRepresentationParameter* representation)
|
|
{
|
|
E_TransferSyntax xfer;
|
|
if (!LookupDcmtkTransferSyntax(xfer, syntax))
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
else
|
|
{
|
|
DicomTransferSyntax sourceSyntax;
|
|
bool known = LookupOrthancTransferSyntax(sourceSyntax, dicom);
|
|
|
|
if (!dicom.chooseRepresentation(xfer, representation).good() ||
|
|
!dicom.canWriteXfer(xfer) ||
|
|
!dicom.validateMetaInfo(xfer, EWM_updateMeta).good())
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
dicom.removeInvalidGroups();
|
|
|
|
if (known)
|
|
{
|
|
CLOG(INFO, DICOM) << "Transcoded an image from transfer syntax "
|
|
<< GetTransferSyntaxUid(sourceSyntax) << " to "
|
|
<< GetTransferSyntaxUid(syntax);
|
|
}
|
|
else
|
|
{
|
|
CLOG(INFO, DICOM) << "Transcoded an image from unknown transfer syntax to "
|
|
<< GetTransferSyntaxUid(syntax);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
ValueRepresentation FromDcmtkBridge::LookupValueRepresentation(const DicomTag& tag)
|
|
{
|
|
DcmTag t(tag.GetGroup(), tag.GetElement());
|
|
return Convert(t.getEVR());
|
|
}
|
|
|
|
ValueRepresentation FromDcmtkBridge::Convert(const DcmEVR vr)
|
|
{
|
|
switch (vr)
|
|
{
|
|
case EVR_AE:
|
|
return ValueRepresentation_ApplicationEntity;
|
|
|
|
case EVR_AS:
|
|
return ValueRepresentation_AgeString;
|
|
|
|
case EVR_AT:
|
|
return ValueRepresentation_AttributeTag;
|
|
|
|
case EVR_CS:
|
|
return ValueRepresentation_CodeString;
|
|
|
|
case EVR_DA:
|
|
return ValueRepresentation_Date;
|
|
|
|
case EVR_DS:
|
|
return ValueRepresentation_DecimalString;
|
|
|
|
case EVR_DT:
|
|
return ValueRepresentation_DateTime;
|
|
|
|
case EVR_FL:
|
|
return ValueRepresentation_FloatingPointSingle;
|
|
|
|
case EVR_FD:
|
|
return ValueRepresentation_FloatingPointDouble;
|
|
|
|
case EVR_IS:
|
|
return ValueRepresentation_IntegerString;
|
|
|
|
case EVR_LO:
|
|
return ValueRepresentation_LongString;
|
|
|
|
case EVR_LT:
|
|
return ValueRepresentation_LongText;
|
|
|
|
case EVR_OB:
|
|
return ValueRepresentation_OtherByte;
|
|
|
|
#if DCMTK_VERSION_NUMBER >= 361
|
|
case EVR_OD:
|
|
return ValueRepresentation_OtherDouble;
|
|
#endif
|
|
|
|
case EVR_OF:
|
|
return ValueRepresentation_OtherFloat;
|
|
|
|
#if DCMTK_VERSION_NUMBER >= 362
|
|
case EVR_OL:
|
|
return ValueRepresentation_OtherLong;
|
|
#endif
|
|
|
|
case EVR_OW:
|
|
return ValueRepresentation_OtherWord;
|
|
|
|
case EVR_PN:
|
|
return ValueRepresentation_PersonName;
|
|
|
|
case EVR_SH:
|
|
return ValueRepresentation_ShortString;
|
|
|
|
case EVR_SL:
|
|
return ValueRepresentation_SignedLong;
|
|
|
|
case EVR_SQ:
|
|
return ValueRepresentation_Sequence;
|
|
|
|
case EVR_SS:
|
|
return ValueRepresentation_SignedShort;
|
|
|
|
case EVR_ST:
|
|
return ValueRepresentation_ShortText;
|
|
|
|
case EVR_TM:
|
|
return ValueRepresentation_Time;
|
|
|
|
#if DCMTK_VERSION_NUMBER >= 361
|
|
case EVR_UC:
|
|
return ValueRepresentation_UnlimitedCharacters;
|
|
#endif
|
|
|
|
case EVR_UI:
|
|
return ValueRepresentation_UniqueIdentifier;
|
|
|
|
case EVR_UL:
|
|
return ValueRepresentation_UnsignedLong;
|
|
|
|
case EVR_UN:
|
|
return ValueRepresentation_Unknown;
|
|
|
|
#if DCMTK_VERSION_NUMBER >= 361
|
|
case EVR_UR:
|
|
return ValueRepresentation_UniversalResource;
|
|
#endif
|
|
|
|
case EVR_US:
|
|
return ValueRepresentation_UnsignedShort;
|
|
|
|
case EVR_UT:
|
|
return ValueRepresentation_UnlimitedText;
|
|
|
|
default:
|
|
return ValueRepresentation_NotSupported;
|
|
}
|
|
}
|
|
|
|
|
|
DcmElement* FromDcmtkBridge::CreateElementForTag(const DicomTag& tag,
|
|
const std::string& privateCreator)
|
|
{
|
|
if (tag.IsPrivate() &&
|
|
privateCreator.empty())
|
|
{
|
|
// This solves issue 140 (Modifying private tags with REST API
|
|
// changes VR from LO to UN)
|
|
// https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=140
|
|
LOG(WARNING) << "Private creator should not be empty while creating a private tag: " << tag.Format();
|
|
}
|
|
|
|
#if DCMTK_VERSION_NUMBER >= 362
|
|
DcmTag key(tag.GetGroup(), tag.GetElement());
|
|
if (tag.IsPrivate())
|
|
{
|
|
return DcmItem::newDicomElement(key, privateCreator.c_str());
|
|
}
|
|
else
|
|
{
|
|
return DcmItem::newDicomElement(key, NULL);
|
|
}
|
|
|
|
#else
|
|
DcmTag key(tag.GetGroup(), tag.GetElement());
|
|
if (tag.IsPrivate())
|
|
{
|
|
// https://forum.dcmtk.org/viewtopic.php?t=4527
|
|
LOG(WARNING) << "You are using DCMTK <= 3.6.1: All the private tags "
|
|
"are considered as having a binary value representation";
|
|
key.setPrivateCreator(privateCreator.c_str());
|
|
return new DcmOtherByteOtherWord(key);
|
|
}
|
|
else
|
|
{
|
|
return newDicomElement(key);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
|
|
|
|
void FromDcmtkBridge::FillElementWithString(DcmElement& element,
|
|
const std::string& utf8Value,
|
|
bool decodeDataUriScheme,
|
|
Encoding dicomEncoding)
|
|
{
|
|
std::string binary;
|
|
const std::string* decoded = &utf8Value;
|
|
|
|
if (decodeDataUriScheme &&
|
|
boost::starts_with(utf8Value, URI_SCHEME_PREFIX_BINARY))
|
|
{
|
|
std::string mime;
|
|
if (!Toolbox::DecodeDataUriScheme(mime, binary, utf8Value))
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat);
|
|
}
|
|
|
|
decoded = &binary;
|
|
}
|
|
else if (dicomEncoding != Encoding_Utf8)
|
|
{
|
|
binary = Toolbox::ConvertFromUtf8(utf8Value, dicomEncoding);
|
|
decoded = &binary;
|
|
}
|
|
|
|
if (IsBinaryTag(element.getTag()))
|
|
{
|
|
bool ok;
|
|
|
|
switch (element.getTag().getEVR())
|
|
{
|
|
case EVR_OW:
|
|
if (decoded->size() % sizeof(Uint16) != 0)
|
|
{
|
|
LOG(ERROR) << "A tag with OW VR must have an even number of bytes";
|
|
ok = false;
|
|
}
|
|
else
|
|
{
|
|
ok = element.putUint16Array((const Uint16*) decoded->c_str(), decoded->size() / sizeof(Uint16)).good();
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
ok = element.putUint8Array((const Uint8*) decoded->c_str(), decoded->size()).good();
|
|
break;
|
|
}
|
|
|
|
if (ok)
|
|
{
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
}
|
|
|
|
bool ok = false;
|
|
|
|
try
|
|
{
|
|
switch (element.getTag().getEVR())
|
|
{
|
|
// http://support.dcmtk.org/docs/dcvr_8h-source.html
|
|
|
|
/**
|
|
* TODO.
|
|
**/
|
|
|
|
case EVR_OB: // other byte
|
|
case EVR_OW: // other word
|
|
throw OrthancException(ErrorCode_NotImplemented);
|
|
|
|
case EVR_UN: // unknown value representation
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
|
|
|
|
/**
|
|
* String types.
|
|
**/
|
|
|
|
case EVR_DS: // decimal string
|
|
case EVR_IS: // integer string
|
|
case EVR_AS: // age string
|
|
case EVR_DA: // date string
|
|
case EVR_DT: // date time string
|
|
case EVR_TM: // time string
|
|
case EVR_AE: // application entity title
|
|
case EVR_CS: // code string
|
|
case EVR_SH: // short string
|
|
case EVR_LO: // long string
|
|
case EVR_ST: // short text
|
|
case EVR_LT: // long text
|
|
case EVR_UT: // unlimited text
|
|
case EVR_PN: // person name
|
|
case EVR_UI: // unique identifier
|
|
#if DCMTK_VERSION_NUMBER >= 361
|
|
case EVR_UC: // unlimited characters
|
|
case EVR_UR: // URI/URL
|
|
#endif
|
|
{
|
|
ok = element.putString(decoded->c_str()).good();
|
|
break;
|
|
}
|
|
|
|
|
|
/**
|
|
* Numerical types
|
|
**/
|
|
|
|
case EVR_SL: // signed long
|
|
{
|
|
if (decoded->find('\\') != std::string::npos)
|
|
{
|
|
ok = element.putString(decoded->c_str()).good();
|
|
}
|
|
else
|
|
{
|
|
ok = element.putSint32(boost::lexical_cast<Sint32>(*decoded)).good();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case EVR_SS: // signed short
|
|
{
|
|
if (decoded->find('\\') != std::string::npos)
|
|
{
|
|
ok = element.putString(decoded->c_str()).good();
|
|
}
|
|
else
|
|
{
|
|
ok = element.putSint16(boost::lexical_cast<Sint16>(*decoded)).good();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case EVR_UL: // unsigned long
|
|
#if DCMTK_VERSION_NUMBER >= 362
|
|
case EVR_OL: // other long (requires byte-swapping)
|
|
#endif
|
|
{
|
|
if (decoded->find('\\') != std::string::npos)
|
|
{
|
|
ok = element.putString(decoded->c_str()).good();
|
|
}
|
|
else
|
|
{
|
|
ok = element.putUint32(boost::lexical_cast<Uint32>(*decoded)).good();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case EVR_xs: // unsigned short, signed short or multiple values
|
|
{
|
|
if (decoded->find('\\') != std::string::npos)
|
|
{
|
|
ok = element.putString(decoded->c_str()).good();
|
|
}
|
|
else if (decoded->find('-') != std::string::npos)
|
|
{
|
|
ok = element.putSint16(boost::lexical_cast<Sint16>(*decoded)).good();
|
|
}
|
|
else
|
|
{
|
|
ok = element.putUint16(boost::lexical_cast<Uint16>(*decoded)).good();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case EVR_US: // unsigned short
|
|
{
|
|
if (decoded->find('\\') != std::string::npos)
|
|
{
|
|
ok = element.putString(decoded->c_str()).good();
|
|
}
|
|
else
|
|
{
|
|
ok = element.putUint16(boost::lexical_cast<Uint16>(*decoded)).good();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case EVR_FL: // float single-precision
|
|
case EVR_OF: // other float (requires byte swapping)
|
|
{
|
|
if (decoded->find('\\') != std::string::npos)
|
|
{
|
|
ok = element.putString(decoded->c_str()).good();
|
|
}
|
|
else
|
|
{
|
|
ok = element.putFloat32(boost::lexical_cast<float>(*decoded)).good();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case EVR_FD: // float double-precision
|
|
#if DCMTK_VERSION_NUMBER >= 361
|
|
case EVR_OD: // other double (requires byte-swapping)
|
|
#endif
|
|
{
|
|
if (decoded->find('\\') != std::string::npos)
|
|
{
|
|
ok = element.putString(decoded->c_str()).good();
|
|
}
|
|
else
|
|
{
|
|
ok = element.putFloat64(boost::lexical_cast<double>(*decoded)).good();
|
|
}
|
|
break;
|
|
}
|
|
|
|
|
|
/**
|
|
* Other types
|
|
**/
|
|
|
|
case EVR_AT: // attribute tag, new in Orthanc 1.9.4
|
|
{
|
|
DicomTag value = ParseTag(utf8Value);
|
|
ok = element.putTagVal(DcmTagKey(value.GetGroup(), value.GetElement())).good();
|
|
break;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sequence types, should never occur at this point.
|
|
**/
|
|
|
|
case EVR_SQ: // sequence of items
|
|
{
|
|
ok = false;
|
|
break;
|
|
}
|
|
|
|
|
|
/**
|
|
* Internal to DCMTK.
|
|
**/
|
|
|
|
case EVR_ox: // OB or OW depending on context
|
|
case EVR_lt: // US, SS or OW depending on context, used for LUT Data (thus the name)
|
|
case EVR_na: // na="not applicable", for data which has no VR
|
|
case EVR_up: // up="unsigned pointer", used internally for DICOMDIR suppor
|
|
case EVR_item: // used internally for items
|
|
case EVR_metainfo: // used internally for meta info datasets
|
|
case EVR_dataset: // used internally for datasets
|
|
case EVR_fileFormat: // used internally for DICOM files
|
|
case EVR_dicomDir: // used internally for DICOMDIR objects
|
|
case EVR_dirRecord: // used internally for DICOMDIR records
|
|
case EVR_pixelSQ: // used internally for pixel sequences in a compressed image
|
|
case EVR_pixelItem: // used internally for pixel items in a compressed image
|
|
case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
|
|
case EVR_PixelData: // used internally for uncompressed pixeld data
|
|
case EVR_OverlayData: // used internally for overlay data
|
|
case EVR_UNKNOWN2B: // used internally for elements with unknown VR with 2-byte length field in explicit VR
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
catch (boost::bad_lexical_cast&)
|
|
{
|
|
ok = false;
|
|
}
|
|
|
|
if (!ok)
|
|
{
|
|
DicomTag tag(element.getTag().getGroup(), element.getTag().getElement());
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"While creating a DICOM instance, tag (" + tag.Format() +
|
|
") has out-of-range value: \"" + (*decoded) + "\"");
|
|
}
|
|
}
|
|
|
|
|
|
DcmElement* FromDcmtkBridge::FromJson(const DicomTag& tag,
|
|
const Json::Value& value,
|
|
bool decodeDataUriScheme,
|
|
Encoding dicomEncoding,
|
|
const std::string& privateCreator)
|
|
{
|
|
std::unique_ptr<DcmElement> element;
|
|
|
|
switch (value.type())
|
|
{
|
|
case Json::stringValue:
|
|
element.reset(CreateElementForTag(tag, privateCreator));
|
|
FillElementWithString(*element, value.asString(), decodeDataUriScheme, dicomEncoding);
|
|
break;
|
|
|
|
case Json::nullValue:
|
|
element.reset(CreateElementForTag(tag, privateCreator));
|
|
FillElementWithString(*element, "", decodeDataUriScheme, dicomEncoding);
|
|
break;
|
|
|
|
case Json::arrayValue:
|
|
{
|
|
const char* p = NULL;
|
|
if (tag.IsPrivate() &&
|
|
!privateCreator.empty())
|
|
{
|
|
p = privateCreator.c_str();
|
|
}
|
|
|
|
DcmTag key(tag.GetGroup(), tag.GetElement(), p);
|
|
if (key.getEVR() != EVR_SQ)
|
|
{
|
|
throw OrthancException(ErrorCode_BadParameterType,
|
|
"Bad Parameter type for tag " + tag.Format());
|
|
}
|
|
|
|
DcmSequenceOfItems* sequence = new DcmSequenceOfItems(key);
|
|
element.reset(sequence);
|
|
|
|
for (Json::Value::ArrayIndex i = 0; i < value.size(); i++)
|
|
{
|
|
std::unique_ptr<DcmItem> item(new DcmItem);
|
|
|
|
switch (value[i].type())
|
|
{
|
|
case Json::objectValue:
|
|
{
|
|
Json::Value::Members members = value[i].getMemberNames();
|
|
for (Json::Value::ArrayIndex j = 0; j < members.size(); j++)
|
|
{
|
|
item->insert(FromJson(ParseTag(members[j]), value[i][members[j]], decodeDataUriScheme, dicomEncoding, privateCreator));
|
|
}
|
|
break;
|
|
}
|
|
|
|
case Json::arrayValue:
|
|
{
|
|
// Lua cannot disambiguate between an empty dictionary
|
|
// and an empty array
|
|
if (value[i].size() != 0)
|
|
{
|
|
throw OrthancException(ErrorCode_BadParameterType);
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
throw OrthancException(ErrorCode_BadParameterType);
|
|
}
|
|
|
|
sequence->append(item.release());
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
throw OrthancException(ErrorCode_BadParameterType, "Bad Parameter type for tag " + tag.Format());
|
|
}
|
|
|
|
return element.release();
|
|
}
|
|
|
|
|
|
DcmPixelSequence* FromDcmtkBridge::GetPixelSequence(DcmDataset& dataset)
|
|
{
|
|
DcmElement *element = NULL;
|
|
if (!dataset.findAndGetElement(DCM_PixelData, element).good())
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat);
|
|
}
|
|
|
|
DcmPixelData& pixelData = dynamic_cast<DcmPixelData&>(*element);
|
|
|
|
E_TransferSyntax repType;
|
|
const DcmRepresentationParameter *repParam = NULL;
|
|
pixelData.getCurrentRepresentationKey(repType, repParam);
|
|
|
|
DcmPixelSequence* pixelSequence = NULL;
|
|
if (!pixelData.getEncapsulatedRepresentation(repType, repParam, pixelSequence).good())
|
|
{
|
|
return NULL;
|
|
}
|
|
else
|
|
{
|
|
return pixelSequence;
|
|
}
|
|
}
|
|
|
|
|
|
Encoding FromDcmtkBridge::ExtractEncoding(const Json::Value& json,
|
|
Encoding defaultEncoding)
|
|
{
|
|
if (json.type() != Json::objectValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadParameterType);
|
|
}
|
|
|
|
Encoding encoding = defaultEncoding;
|
|
|
|
const Json::Value::Members tags = json.getMemberNames();
|
|
|
|
// Look for SpecificCharacterSet (0008,0005) in the JSON file
|
|
for (size_t i = 0; i < tags.size(); i++)
|
|
{
|
|
DicomTag tag = FromDcmtkBridge::ParseTag(tags[i]);
|
|
if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
|
|
{
|
|
const Json::Value& value = json[tags[i]];
|
|
if (value.type() != Json::stringValue ||
|
|
(value.asString().length() != 0 &&
|
|
!GetDicomEncoding(encoding, value.asCString())))
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest,
|
|
"Unknown encoding while creating DICOM from JSON: " +
|
|
value.toStyledString());
|
|
}
|
|
|
|
if (value.asString().length() == 0)
|
|
{
|
|
return defaultEncoding;
|
|
}
|
|
}
|
|
}
|
|
|
|
return encoding;
|
|
}
|
|
|
|
|
|
static void SetString(DcmDataset& target,
|
|
const DcmTag& tag,
|
|
const std::string& value)
|
|
{
|
|
if (!target.putAndInsertString(tag, value.c_str()).good())
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
}
|
|
|
|
|
|
DcmDataset* FromDcmtkBridge::FromJson(const Json::Value& json, // Encoded using UTF-8
|
|
bool generateIdentifiers,
|
|
bool decodeDataUriScheme,
|
|
Encoding defaultEncoding,
|
|
const std::string& privateCreator)
|
|
{
|
|
std::unique_ptr<DcmDataset> result(new DcmDataset);
|
|
Encoding encoding = ExtractEncoding(json, defaultEncoding);
|
|
|
|
SetString(*result, DCM_SpecificCharacterSet, GetDicomSpecificCharacterSet(encoding));
|
|
|
|
const Json::Value::Members tags = json.getMemberNames();
|
|
|
|
bool hasPatientId = false;
|
|
bool hasStudyInstanceUid = false;
|
|
bool hasSeriesInstanceUid = false;
|
|
bool hasSopInstanceUid = false;
|
|
|
|
for (size_t i = 0; i < tags.size(); i++)
|
|
{
|
|
DicomTag tag = FromDcmtkBridge::ParseTag(tags[i]);
|
|
const Json::Value& value = json[tags[i]];
|
|
|
|
if (tag == DICOM_TAG_PATIENT_ID)
|
|
{
|
|
hasPatientId = true;
|
|
}
|
|
else if (tag == DICOM_TAG_STUDY_INSTANCE_UID)
|
|
{
|
|
hasStudyInstanceUid = true;
|
|
}
|
|
else if (tag == DICOM_TAG_SERIES_INSTANCE_UID)
|
|
{
|
|
hasSeriesInstanceUid = true;
|
|
}
|
|
else if (tag == DICOM_TAG_SOP_INSTANCE_UID)
|
|
{
|
|
hasSopInstanceUid = true;
|
|
}
|
|
|
|
if (tag != DICOM_TAG_SPECIFIC_CHARACTER_SET)
|
|
{
|
|
std::unique_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator));
|
|
|
|
result->findAndDeleteElement(element->getTag());
|
|
|
|
DcmElement* tmp = element.release();
|
|
if (!result->insert(tmp, false, false).good())
|
|
{
|
|
delete tmp;
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasPatientId &&
|
|
generateIdentifiers)
|
|
{
|
|
SetString(*result, DCM_PatientID, GenerateUniqueIdentifier(ResourceType_Patient));
|
|
}
|
|
|
|
if (!hasStudyInstanceUid &&
|
|
generateIdentifiers)
|
|
{
|
|
SetString(*result, DCM_StudyInstanceUID, GenerateUniqueIdentifier(ResourceType_Study));
|
|
}
|
|
|
|
if (!hasSeriesInstanceUid &&
|
|
generateIdentifiers)
|
|
{
|
|
SetString(*result, DCM_SeriesInstanceUID, GenerateUniqueIdentifier(ResourceType_Series));
|
|
}
|
|
|
|
if (!hasSopInstanceUid &&
|
|
generateIdentifiers)
|
|
{
|
|
SetString(*result, DCM_SOPInstanceUID, GenerateUniqueIdentifier(ResourceType_Instance));
|
|
}
|
|
|
|
return result.release();
|
|
}
|
|
|
|
|
|
DcmFileFormat* FromDcmtkBridge::LoadFromMemoryBuffer(const void* buffer,
|
|
size_t size)
|
|
{
|
|
DcmInputBufferStream is;
|
|
if (size > 0)
|
|
{
|
|
is.setBuffer(buffer, size);
|
|
}
|
|
is.setEos();
|
|
|
|
std::unique_ptr<DcmFileFormat> result(new DcmFileFormat);
|
|
|
|
result->transferInit();
|
|
|
|
/**
|
|
* New in Orthanc 1.6.0: The "size" is given as an argument to the
|
|
* "read()" method. This can avoid huge memory consumption if
|
|
* parsing an invalid DICOM file, which can notably been observed
|
|
* by executing the integration test "test_upload_compressed" on
|
|
* valgrind running Orthanc.
|
|
**/
|
|
if (!result->read(is, EXS_Unknown, EGL_noChange, size).good())
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"Cannot parse an invalid DICOM file (size: " +
|
|
boost::lexical_cast<std::string>(size) + " bytes)");
|
|
}
|
|
|
|
result->loadAllDataIntoMemory();
|
|
result->transferEnd();
|
|
|
|
return result.release();
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::FromJson(DicomMap& target,
|
|
const Json::Value& source,
|
|
const char* fieldName)
|
|
{
|
|
if (source.type() != Json::objectValue)
|
|
{
|
|
if (fieldName != NULL)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat, std::string("Expecting an object in field '") + std::string(fieldName) + std::string("'"));
|
|
}
|
|
else
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat, "Expecting an object");
|
|
}
|
|
}
|
|
|
|
target.Clear();
|
|
|
|
Json::Value::Members members = source.getMemberNames();
|
|
|
|
for (size_t i = 0; i < members.size(); i++)
|
|
{
|
|
const Json::Value& value = source[members[i]];
|
|
|
|
if (value.type() != Json::stringValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat, std::string("Expecting a string in field '") + members[i] + std::string("'"));
|
|
}
|
|
|
|
target.SetValue(ParseTag(members[i]), value.asString(), false);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
void FromDcmtkBridge::ChangeStringEncoding(DcmItem& dataset,
|
|
Encoding source,
|
|
bool hasSourceCodeExtensions,
|
|
Encoding target)
|
|
{
|
|
// Recursive exploration of a dataset to change the encoding of
|
|
// each string-like element
|
|
|
|
if (source == target)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (unsigned long i = 0; i < dataset.card(); i++)
|
|
{
|
|
DcmElement* element = dataset.getElement(i);
|
|
if (element)
|
|
{
|
|
if (element->isLeaf())
|
|
{
|
|
char *c = NULL;
|
|
if (element->isaString() &&
|
|
element->getString(c).good() &&
|
|
c != NULL)
|
|
{
|
|
std::string a = Toolbox::ConvertToUtf8(c, source, hasSourceCodeExtensions);
|
|
std::string b = Toolbox::ConvertFromUtf8(a, target);
|
|
element->putString(b.c_str());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// "All subclasses of DcmElement except for DcmSequenceOfItems
|
|
// are leaf nodes, while DcmSequenceOfItems, DcmItem, DcmDataset
|
|
// etc. are not." The following dynamic_cast is thus OK.
|
|
DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(*element);
|
|
|
|
for (unsigned long j = 0; j < sequence.card(); j++)
|
|
{
|
|
ChangeStringEncoding(*sequence.getItem(j), source, hasSourceCodeExtensions, target);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::InitializeCodecs()
|
|
{
|
|
#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
|
|
CLOG(INFO, DICOM) << "Registering JPEG Lossless codecs in DCMTK";
|
|
DJLSDecoderRegistration::registerCodecs();
|
|
# if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
|
|
DJLSEncoderRegistration::registerCodecs();
|
|
# endif
|
|
#endif
|
|
|
|
#if ORTHANC_ENABLE_DCMTK_JPEG == 1
|
|
CLOG(INFO, DICOM) << "Registering JPEG codecs in DCMTK";
|
|
DJDecoderRegistration::registerCodecs();
|
|
# if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
|
|
DJEncoderRegistration::registerCodecs();
|
|
# endif
|
|
#endif
|
|
|
|
CLOG(INFO, DICOM) << "Registering RLE codecs in DCMTK";
|
|
DcmRLEDecoderRegistration::registerCodecs();
|
|
#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
|
|
DcmRLEEncoderRegistration::registerCodecs();
|
|
#endif
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::FinalizeCodecs()
|
|
{
|
|
#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
|
|
// Unregister JPEG-LS codecs
|
|
DJLSDecoderRegistration::cleanup();
|
|
# if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
|
|
DJLSEncoderRegistration::cleanup();
|
|
# endif
|
|
#endif
|
|
|
|
#if ORTHANC_ENABLE_DCMTK_JPEG == 1
|
|
// Unregister JPEG codecs
|
|
DJDecoderRegistration::cleanup();
|
|
# if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
|
|
DJEncoderRegistration::cleanup();
|
|
# endif
|
|
#endif
|
|
|
|
DcmRLEDecoderRegistration::cleanup();
|
|
#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
|
|
DcmRLEEncoderRegistration::cleanup();
|
|
#endif
|
|
}
|
|
|
|
|
|
|
|
// Forward declaration
|
|
static bool ApplyVisitorToElement(DcmElement& element,
|
|
ITagVisitor& visitor,
|
|
const std::vector<DicomTag>& parentTags,
|
|
const std::vector<size_t>& parentIndexes,
|
|
Encoding encoding,
|
|
bool hasCodeExtensions);
|
|
|
|
static void ApplyVisitorToDataset(DcmItem& dataset,
|
|
ITagVisitor& visitor,
|
|
const std::vector<DicomTag>& parentTags,
|
|
const std::vector<size_t>& parentIndexes,
|
|
Encoding encoding,
|
|
bool hasCodeExtensions)
|
|
{
|
|
assert(parentTags.size() == parentIndexes.size());
|
|
|
|
std::set<DcmTagKey> toRemove;
|
|
|
|
for (unsigned long i = 0; i < dataset.card(); i++)
|
|
{
|
|
DcmElement* element = dataset.getElement(i);
|
|
if (element == NULL)
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
else
|
|
{
|
|
if (!ApplyVisitorToElement(*element, visitor, parentTags, parentIndexes, encoding, hasCodeExtensions))
|
|
{
|
|
toRemove.insert(element->getTag());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove all the tags that were planned for removal (cf. ITagVisitor::Action_Remove)
|
|
for (std::set<DcmTagKey>::const_iterator
|
|
it = toRemove.begin(); it != toRemove.end(); ++it)
|
|
{
|
|
std::unique_ptr<DcmElement> tmp(dataset.remove(*it));
|
|
}
|
|
}
|
|
|
|
|
|
// Returns "true" iff the element must be kept. If "false" is
|
|
// returned, the element will be removed.
|
|
static bool ApplyVisitorToLeaf(DcmElement& element,
|
|
ITagVisitor& visitor,
|
|
const std::vector<DicomTag>& parentTags,
|
|
const std::vector<size_t>& parentIndexes,
|
|
const DicomTag& tag,
|
|
Encoding encoding,
|
|
bool hasCodeExtensions)
|
|
{
|
|
// TODO - Merge this function, that is more recent, with ConvertLeafElement()
|
|
|
|
assert(element.isLeaf());
|
|
|
|
DcmEVR evr = element.getTag().getEVR();
|
|
|
|
|
|
/**
|
|
* Fix the EVR for types internal to DCMTK
|
|
**/
|
|
|
|
if (evr == EVR_ox) // OB or OW depending on context
|
|
{
|
|
evr = EVR_OB;
|
|
}
|
|
else if (evr == EVR_xs) // SS or US depending on context
|
|
{
|
|
// So far we assume that it's alway US (as a best guess: https://forum.dcmtk.org/viewtopic.php?t=932)
|
|
// However, e.g. in a LUTDescriptor (3 values), the middle value can be a SS depending on other tag values while first and third value are always US.
|
|
// This patch, although not perfect fixes https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=214.
|
|
// It might need some rework once we encounter a LUTDescriptor with a SS value. ref: https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.11.2.html#sect_C.11.2.1.1
|
|
evr = EVR_US;
|
|
}
|
|
else if (evr == EVR_lt) // US, SS or OW depending on context, used for LUT Data (thus the name)
|
|
{
|
|
// best guess is OW: final user should be able to interpret it correctly depending on the context
|
|
evr = EVR_OW;
|
|
}
|
|
|
|
if (evr == EVR_UNKNOWN || // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
|
|
evr == EVR_UNKNOWN2B) // used internally for elements with unknown VR with 2-byte length field in explicit VR
|
|
{
|
|
evr = EVR_UN;
|
|
}
|
|
|
|
if (evr == EVR_UN)
|
|
{
|
|
// New in Orthanc 1.9.5
|
|
FromDcmtkBridge::DictionaryReaderLock lock;
|
|
|
|
// The "entry" value is only valid while "lock" is active
|
|
const DcmDictEntry* entry = lock.GetDictionary().findEntry(element.getTag().getXTag(),
|
|
element.getTag().getPrivateCreator());
|
|
|
|
if (entry != NULL)
|
|
{
|
|
evr = entry->getEVR();
|
|
}
|
|
}
|
|
|
|
const ValueRepresentation vr = FromDcmtkBridge::Convert(evr);
|
|
|
|
|
|
/**
|
|
* Deal with binary data (including PixelData).
|
|
**/
|
|
|
|
if (evr == EVR_OB || // other byte
|
|
evr == EVR_OW || // other word
|
|
evr == EVR_UN) // unknown value representation
|
|
{
|
|
Uint16* data16 = NULL;
|
|
Uint8* data = NULL;
|
|
|
|
ITagVisitor::Action action;
|
|
|
|
if ((element.getTag() == DCM_PixelData || // (*) New in Orthanc 1.9.1
|
|
evr == EVR_OW) &&
|
|
element.getUint16Array(data16) == EC_Normal)
|
|
{
|
|
action = visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data16, element.getLength());
|
|
}
|
|
else if (evr != EVR_OW &&
|
|
element.getUint8Array(data) == EC_Normal)
|
|
{
|
|
/**
|
|
* WARNING: The call to "getUint8Array()" crashes
|
|
* (segmentation fault) on big-endian architectures if applied
|
|
* to pixel data, during the call to "swapIfNecessary()" in
|
|
* "DcmPolymorphOBOW::getUint8Array()" (this method is not
|
|
* reimplemented in derived class "DcmPixelData"). However,
|
|
* "getUint16Array()" works correctly, hence (*).
|
|
**/
|
|
action = visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data, element.getLength());
|
|
}
|
|
else
|
|
{
|
|
action = visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
|
|
}
|
|
|
|
switch (action)
|
|
{
|
|
case ITagVisitor::Action_None:
|
|
return true; // We're done
|
|
|
|
case ITagVisitor::Action_Remove:
|
|
return false;
|
|
|
|
case ITagVisitor::Action_Replace:
|
|
throw OrthancException(ErrorCode_NotImplemented, "Iterator cannot replace binary data");
|
|
|
|
default:
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Deal with plain strings (and convert them to UTF-8)
|
|
**/
|
|
|
|
char *c = NULL;
|
|
if (element.isaString() &&
|
|
element.getString(c).good())
|
|
{
|
|
std::string utf8;
|
|
|
|
if (c != NULL) // This case corresponds to the empty string
|
|
{
|
|
if (element.getTag() == DCM_SpecificCharacterSet)
|
|
{
|
|
utf8.assign(c);
|
|
}
|
|
else
|
|
{
|
|
std::string s(c);
|
|
utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions);
|
|
}
|
|
}
|
|
|
|
std::string newValue;
|
|
ITagVisitor::Action action = visitor.VisitString
|
|
(newValue, parentTags, parentIndexes, tag, vr, utf8);
|
|
|
|
switch (action)
|
|
{
|
|
case ITagVisitor::Action_None:
|
|
return true;
|
|
|
|
case ITagVisitor::Action_Remove:
|
|
return false;
|
|
|
|
case ITagVisitor::Action_Replace:
|
|
{
|
|
std::string s = Toolbox::ConvertFromUtf8(newValue, encoding);
|
|
if (element.putString(s.c_str()) != EC_Normal)
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError,
|
|
"Iterator cannot replace value of tag: " + tag.Format());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
default:
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
}
|
|
|
|
|
|
ITagVisitor::Action action;
|
|
|
|
try
|
|
{
|
|
// http://support.dcmtk.org/docs/dcvr_8h-source.html
|
|
switch (evr)
|
|
{
|
|
|
|
/**
|
|
* Plain string values.
|
|
**/
|
|
|
|
case EVR_DS: // decimal string
|
|
case EVR_IS: // integer string
|
|
case EVR_AS: // age string
|
|
case EVR_DA: // date string
|
|
case EVR_DT: // date time string
|
|
case EVR_TM: // time string
|
|
case EVR_AE: // application entity title
|
|
case EVR_CS: // code string
|
|
case EVR_SH: // short string
|
|
case EVR_LO: // long string
|
|
case EVR_ST: // short text
|
|
case EVR_LT: // long text
|
|
case EVR_UT: // unlimited text
|
|
case EVR_PN: // person name
|
|
case EVR_UI: // unique identifier
|
|
{
|
|
Uint8* data = NULL;
|
|
|
|
if (element.getUint8Array(data) == EC_Normal)
|
|
{
|
|
const Uint32 length = element.getLength();
|
|
Uint32 l = 0;
|
|
while (l < length &&
|
|
data[l] != 0)
|
|
{
|
|
l++;
|
|
}
|
|
|
|
std::string ignored;
|
|
std::string s(reinterpret_cast<const char*>(data), l);
|
|
action = visitor.VisitString(ignored, parentTags, parentIndexes, tag, vr,
|
|
Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions));
|
|
}
|
|
else
|
|
{
|
|
action = visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
|
|
}
|
|
|
|
if (action == ITagVisitor::Action_Replace)
|
|
{
|
|
LOG(WARNING) << "Iterator cannot replace this string tag: "
|
|
<< FromDcmtkBridge::GetTagName(element)
|
|
<< " (" << tag.Format() << ")";
|
|
return true;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
/**
|
|
* Numeric types
|
|
**/
|
|
|
|
case EVR_SL: // signed long
|
|
{
|
|
DcmSignedLong& content = dynamic_cast<DcmSignedLong&>(element);
|
|
|
|
std::vector<int64_t> values;
|
|
values.reserve(content.getVM());
|
|
|
|
for (unsigned long i = 0; i < content.getVM(); i++)
|
|
{
|
|
Sint32 f;
|
|
if (content.getSint32(f, i).good())
|
|
{
|
|
values.push_back(f);
|
|
}
|
|
}
|
|
|
|
action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
|
|
break;
|
|
}
|
|
|
|
case EVR_SS: // signed short
|
|
{
|
|
DcmSignedShort& content = dynamic_cast<DcmSignedShort&>(element);
|
|
|
|
std::vector<int64_t> values;
|
|
values.reserve(content.getVM());
|
|
|
|
for (unsigned long i = 0; i < content.getVM(); i++)
|
|
{
|
|
Sint16 f;
|
|
if (content.getSint16(f, i).good())
|
|
{
|
|
values.push_back(f);
|
|
}
|
|
}
|
|
|
|
action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
|
|
break;
|
|
}
|
|
|
|
case EVR_UL: // unsigned long
|
|
#if DCMTK_VERSION_NUMBER >= 362
|
|
case EVR_OL:
|
|
#endif
|
|
{
|
|
DcmUnsignedLong& content = dynamic_cast<DcmUnsignedLong&>(element);
|
|
|
|
std::vector<int64_t> values;
|
|
values.reserve(content.getVM());
|
|
|
|
for (unsigned long i = 0; i < content.getVM(); i++)
|
|
{
|
|
Uint32 f;
|
|
if (content.getUint32(f, i).good())
|
|
{
|
|
values.push_back(f);
|
|
}
|
|
}
|
|
|
|
action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
|
|
break;
|
|
}
|
|
|
|
case EVR_US: // unsigned short
|
|
{
|
|
DcmUnsignedShort& content = dynamic_cast<DcmUnsignedShort&>(element);
|
|
|
|
std::vector<int64_t> values;
|
|
values.reserve(content.getVM());
|
|
|
|
for (unsigned long i = 0; i < content.getVM(); i++)
|
|
{
|
|
Uint16 f;
|
|
if (content.getUint16(f, i).good())
|
|
{
|
|
values.push_back(f);
|
|
}
|
|
}
|
|
|
|
action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
|
|
break;
|
|
}
|
|
|
|
case EVR_FL: // float single-precision
|
|
case EVR_OF:
|
|
{
|
|
DcmFloatingPointSingle& content = dynamic_cast<DcmFloatingPointSingle&>(element);
|
|
|
|
std::vector<double> values;
|
|
values.reserve(content.getVM());
|
|
|
|
for (unsigned long i = 0; i < content.getVM(); i++)
|
|
{
|
|
Float32 f;
|
|
if (content.getFloat32(f, i).good())
|
|
{
|
|
values.push_back(f);
|
|
}
|
|
}
|
|
|
|
action = visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values);
|
|
break;
|
|
}
|
|
|
|
case EVR_FD: // float double-precision
|
|
#if DCMTK_VERSION_NUMBER >= 361
|
|
case EVR_OD:
|
|
#endif
|
|
{
|
|
DcmFloatingPointDouble& content = dynamic_cast<DcmFloatingPointDouble&>(element);
|
|
|
|
std::vector<double> values;
|
|
values.reserve(content.getVM());
|
|
|
|
for (unsigned long i = 0; i < content.getVM(); i++)
|
|
{
|
|
Float64 f;
|
|
if (content.getFloat64(f, i).good())
|
|
{
|
|
values.push_back(f);
|
|
}
|
|
}
|
|
|
|
action = visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values);
|
|
break;
|
|
}
|
|
|
|
|
|
/**
|
|
* Attribute tag.
|
|
**/
|
|
|
|
case EVR_AT:
|
|
{
|
|
DcmAttributeTag& content = dynamic_cast<DcmAttributeTag&>(element);
|
|
|
|
std::vector<DicomTag> values;
|
|
values.reserve(content.getVM());
|
|
|
|
for (unsigned long i = 0; i < content.getVM(); i++)
|
|
{
|
|
DcmTagKey f;
|
|
if (content.getTagVal(f, i).good())
|
|
{
|
|
DicomTag t(f.getGroup(), f.getElement());
|
|
values.push_back(t);
|
|
}
|
|
}
|
|
|
|
assert(vr == ValueRepresentation_AttributeTag);
|
|
action = visitor.VisitAttributes(parentTags, parentIndexes, tag, values);
|
|
break;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sequence types, should never occur at this point because of
|
|
* "element.isLeaf()".
|
|
**/
|
|
|
|
case EVR_SQ: // sequence of items
|
|
{
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Internal to DCMTK.
|
|
**/
|
|
|
|
case EVR_xs: // SS or US depending on context
|
|
case EVR_lt: // US, SS or OW depending on context, used for LUT Data (thus the name)
|
|
case EVR_na: // na="not applicable", for data which has no VR
|
|
case EVR_up: // up="unsigned pointer", used internally for DICOMDIR suppor
|
|
case EVR_item: // used internally for items
|
|
case EVR_metainfo: // used internally for meta info datasets
|
|
case EVR_dataset: // used internally for datasets
|
|
case EVR_fileFormat: // used internally for DICOM files
|
|
case EVR_dicomDir: // used internally for DICOMDIR objects
|
|
case EVR_dirRecord: // used internally for DICOMDIR records
|
|
case EVR_pixelSQ: // used internally for pixel sequences in a compressed image
|
|
case EVR_pixelItem: // used internally for pixel items in a compressed image
|
|
case EVR_PixelData: // used internally for uncompressed pixeld data
|
|
case EVR_OverlayData: // used internally for overlay data
|
|
{
|
|
action = visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
|
|
break;
|
|
}
|
|
|
|
|
|
/**
|
|
* Default case.
|
|
**/
|
|
|
|
default:
|
|
return true;
|
|
}
|
|
|
|
switch (action)
|
|
{
|
|
case ITagVisitor::Action_None:
|
|
return true; // We're done
|
|
|
|
case ITagVisitor::Action_Remove:
|
|
return false;
|
|
|
|
case ITagVisitor::Action_Replace:
|
|
throw OrthancException(ErrorCode_NotImplemented, "Iterator cannot replace non-string-like data");
|
|
|
|
default:
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
}
|
|
}
|
|
catch (boost::bad_lexical_cast&)
|
|
{
|
|
return true;
|
|
}
|
|
catch (std::bad_cast&)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
// Returns "true" iff the element must be kept. If "false" is
|
|
// returned, the element will be removed.
|
|
static bool ApplyVisitorToElement(DcmElement& element,
|
|
ITagVisitor& visitor,
|
|
const std::vector<DicomTag>& parentTags,
|
|
const std::vector<size_t>& parentIndexes,
|
|
Encoding encoding,
|
|
bool hasCodeExtensions)
|
|
{
|
|
assert(parentTags.size() == parentIndexes.size());
|
|
|
|
DicomTag tag(FromDcmtkBridge::Convert(element.getTag()));
|
|
|
|
if (element.isLeaf())
|
|
{
|
|
return ApplyVisitorToLeaf(element, visitor, parentTags, parentIndexes, tag, encoding, hasCodeExtensions);
|
|
}
|
|
else
|
|
{
|
|
// "All subclasses of DcmElement except for DcmSequenceOfItems
|
|
// are leaf nodes, while DcmSequenceOfItems, DcmItem, DcmDataset
|
|
// etc. are not." The following dynamic_cast is thus OK.
|
|
DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(element);
|
|
|
|
ITagVisitor::Action action = visitor.VisitSequence(parentTags, parentIndexes, tag, sequence.card());
|
|
|
|
switch (action)
|
|
{
|
|
case ITagVisitor::Action_None:
|
|
if (sequence.card() != 0) // Minor optimization to avoid creating "tags" and "indexes" if not needed
|
|
{
|
|
std::vector<DicomTag> tags = parentTags;
|
|
std::vector<size_t> indexes = parentIndexes;
|
|
tags.push_back(tag);
|
|
indexes.push_back(0);
|
|
|
|
for (unsigned long i = 0; i < sequence.card(); i++)
|
|
{
|
|
indexes.back() = static_cast<size_t>(i);
|
|
DcmItem* child = sequence.getItem(i);
|
|
ApplyVisitorToDataset(*child, visitor, tags, indexes, encoding, hasCodeExtensions);
|
|
}
|
|
}
|
|
|
|
return true; // Keep
|
|
|
|
case ITagVisitor::Action_Remove:
|
|
return false;
|
|
|
|
case ITagVisitor::Action_Replace:
|
|
throw OrthancException(ErrorCode_NotImplemented, "Iterator cannot replace sequences");
|
|
|
|
default:
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::Apply(DcmItem& dataset,
|
|
ITagVisitor& visitor,
|
|
Encoding defaultEncoding)
|
|
{
|
|
std::vector<DicomTag> parentTags;
|
|
std::vector<size_t> parentIndexes;
|
|
bool hasCodeExtensions;
|
|
Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
|
|
ApplyVisitorToDataset(dataset, visitor, parentTags, parentIndexes, encoding, hasCodeExtensions);
|
|
}
|
|
|
|
|
|
|
|
bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target,
|
|
DcmFileFormat& dicom)
|
|
{
|
|
if (dicom.getDataset() == NULL)
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
else
|
|
{
|
|
return LookupOrthancTransferSyntax(target, *dicom.getDataset());
|
|
}
|
|
}
|
|
|
|
|
|
bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target,
|
|
DcmDataset& dataset)
|
|
{
|
|
E_TransferSyntax xfer = dataset.getCurrentXfer();
|
|
if (xfer == EXS_Unknown)
|
|
{
|
|
dataset.updateOriginalXfer();
|
|
xfer = dataset.getOriginalXfer();
|
|
if (xfer == EXS_Unknown)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"Cannot determine the transfer syntax of the DICOM instance");
|
|
}
|
|
}
|
|
|
|
return FromDcmtkBridge::LookupOrthancTransferSyntax(target, xfer);
|
|
}
|
|
|
|
|
|
std::string FromDcmtkBridge::FormatMissingTagsForStore(DcmDataset& dicom)
|
|
{
|
|
std::string patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
|
|
|
|
const char* c = NULL;
|
|
if (dicom.findAndGetString(DCM_PatientID, c).good() &&
|
|
c != NULL)
|
|
{
|
|
patientId.assign(c);
|
|
}
|
|
|
|
c = NULL;
|
|
if (dicom.findAndGetString(DCM_StudyInstanceUID, c).good() &&
|
|
c != NULL)
|
|
{
|
|
studyInstanceUid.assign(c);
|
|
}
|
|
|
|
c = NULL;
|
|
if (dicom.findAndGetString(DCM_SeriesInstanceUID, c).good() &&
|
|
c != NULL)
|
|
{
|
|
seriesInstanceUid.assign(c);
|
|
}
|
|
|
|
c = NULL;
|
|
if (dicom.findAndGetString(DCM_SOPInstanceUID, c).good() &&
|
|
c != NULL)
|
|
{
|
|
sopInstanceUid.assign(c);
|
|
}
|
|
|
|
return DicomMap::FormatMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::IDicomPathVisitor::ApplyInternal(FromDcmtkBridge::IDicomPathVisitor& visitor,
|
|
DcmItem& item,
|
|
const DicomPath& pattern,
|
|
const DicomPath& actualPath)
|
|
{
|
|
const size_t level = actualPath.GetPrefixLength();
|
|
|
|
if (level == pattern.GetPrefixLength())
|
|
{
|
|
visitor.Visit(item, actualPath);
|
|
}
|
|
else
|
|
{
|
|
assert(level < pattern.GetPrefixLength());
|
|
|
|
const DicomTag& tmp = pattern.GetPrefixTag(level);
|
|
DcmTagKey tag(tmp.GetGroup(), tmp.GetElement());
|
|
|
|
DcmSequenceOfItems *sequence = NULL;
|
|
if (item.findAndGetSequence(tag, sequence).good() &&
|
|
sequence != NULL)
|
|
{
|
|
for (unsigned long i = 0; i < sequence->card(); i++)
|
|
{
|
|
if (pattern.IsPrefixUniversal(level) ||
|
|
pattern.GetPrefixIndex(level) == static_cast<size_t>(i))
|
|
{
|
|
DcmItem *child = sequence->getItem(i);
|
|
if (child != NULL)
|
|
{
|
|
DicomPath childPath = actualPath;
|
|
childPath.AddIndexedTagToPrefix(pattern.GetPrefixTag(level), static_cast<size_t>(i));
|
|
|
|
ApplyInternal(visitor, *child, pattern, childPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::IDicomPathVisitor::Apply(IDicomPathVisitor& visitor,
|
|
DcmDataset& dataset,
|
|
const DicomPath& path)
|
|
{
|
|
DicomPath actualPath(path.GetFinalTag());
|
|
ApplyInternal(visitor, dataset, path, actualPath);
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::RemovePath(DcmDataset& dataset,
|
|
const DicomPath& path)
|
|
{
|
|
class Visitor : public FromDcmtkBridge::IDicomPathVisitor
|
|
{
|
|
public:
|
|
virtual void Visit(DcmItem& item,
|
|
const DicomPath& path) ORTHANC_OVERRIDE
|
|
{
|
|
DcmTagKey key(path.GetFinalTag().GetGroup(), path.GetFinalTag().GetElement());
|
|
std::unique_ptr<DcmElement> removed(item.remove(key));
|
|
}
|
|
};
|
|
|
|
Visitor visitor;
|
|
IDicomPathVisitor::Apply(visitor, dataset, path);
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::ClearPath(DcmDataset& dataset,
|
|
const DicomPath& path,
|
|
bool onlyIfExists)
|
|
{
|
|
class Visitor : public FromDcmtkBridge::IDicomPathVisitor
|
|
{
|
|
public:
|
|
bool onlyIfExists_;
|
|
|
|
public:
|
|
explicit Visitor(bool onlyIfExists) :
|
|
onlyIfExists_(onlyIfExists)
|
|
{
|
|
}
|
|
|
|
virtual void Visit(DcmItem& item,
|
|
const DicomPath& path) ORTHANC_OVERRIDE
|
|
{
|
|
DcmTagKey key(path.GetFinalTag().GetGroup(), path.GetFinalTag().GetElement());
|
|
|
|
if (onlyIfExists_ &&
|
|
!item.tagExists(key))
|
|
{
|
|
// The tag is non-existing, do not clear it
|
|
}
|
|
else
|
|
{
|
|
if (!item.insertEmptyElement(key, OFTrue /* replace old value */).good())
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Visitor visitor(onlyIfExists);
|
|
IDicomPathVisitor::Apply(visitor, dataset, path);
|
|
}
|
|
|
|
|
|
void FromDcmtkBridge::ReplacePath(DcmDataset& dataset,
|
|
const DicomPath& path,
|
|
const DcmElement& element,
|
|
DicomReplaceMode mode)
|
|
{
|
|
class Visitor : public FromDcmtkBridge::IDicomPathVisitor
|
|
{
|
|
private:
|
|
std::unique_ptr<DcmElement> element_;
|
|
DicomReplaceMode mode_;
|
|
|
|
public:
|
|
Visitor(const DcmElement& element,
|
|
DicomReplaceMode mode) :
|
|
element_(dynamic_cast<DcmElement*>(element.clone())),
|
|
mode_(mode)
|
|
{
|
|
if (element_.get() == NULL)
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError, "Cannot clone DcmElement");
|
|
}
|
|
}
|
|
|
|
virtual void Visit(DcmItem& item,
|
|
const DicomPath& path) ORTHANC_OVERRIDE
|
|
{
|
|
std::unique_ptr<DcmElement> cloned(dynamic_cast<DcmElement*>(element_->clone()));
|
|
if (cloned.get() == NULL)
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError, "Cannot clone DcmElement");
|
|
}
|
|
else
|
|
{
|
|
DcmTagKey key(path.GetFinalTag().GetGroup(), path.GetFinalTag().GetElement());
|
|
|
|
if (!item.tagExists(key))
|
|
{
|
|
switch (mode_)
|
|
{
|
|
case DicomReplaceMode_InsertIfAbsent:
|
|
break; // Fine, we can proceed with insertion
|
|
|
|
case DicomReplaceMode_ThrowIfAbsent:
|
|
throw OrthancException(ErrorCode_InexistentItem, "Cannot replace inexistent tag: " + GetTagName(*element_));
|
|
|
|
case DicomReplaceMode_IgnoreIfAbsent:
|
|
return; // Don't proceed with insertion
|
|
|
|
default:
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
}
|
|
}
|
|
|
|
if (!item.insert(cloned.release(), OFTrue /* replace old */).good())
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError, "Cannot replace an element: " + GetTagName(*element_));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
DcmTagKey key(path.GetFinalTag().GetGroup(), path.GetFinalTag().GetElement());
|
|
|
|
if (element.getTag() != key)
|
|
{
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange,
|
|
"The final tag must be the same as the tag of the element during a replacement");
|
|
}
|
|
else
|
|
{
|
|
Visitor visitor(element, mode);
|
|
IDicomPathVisitor::Apply(visitor, dataset, path);
|
|
}
|
|
}
|
|
|
|
|
|
bool FromDcmtkBridge::LookupSequenceItem(DicomMap& target,
|
|
DcmDataset& dataset,
|
|
const DicomPath& path,
|
|
size_t sequenceIndex)
|
|
{
|
|
class Visitor : public FromDcmtkBridge::IDicomPathVisitor
|
|
{
|
|
private:
|
|
bool found_;
|
|
DicomMap& target_;
|
|
size_t sequenceIndex_;
|
|
|
|
public:
|
|
Visitor(DicomMap& target,
|
|
size_t sequenceIndex) :
|
|
found_(false),
|
|
target_(target),
|
|
sequenceIndex_(sequenceIndex)
|
|
{
|
|
}
|
|
|
|
virtual void Visit(DcmItem& item,
|
|
const DicomPath& path) ORTHANC_OVERRIDE
|
|
{
|
|
DcmTagKey tag(path.GetFinalTag().GetGroup(), path.GetFinalTag().GetElement());
|
|
|
|
DcmSequenceOfItems *sequence = NULL;
|
|
|
|
if (item.findAndGetSequence(tag, sequence).good() &&
|
|
sequence != NULL &&
|
|
sequenceIndex_ < sequence->card())
|
|
{
|
|
std::set<DicomTag> ignoreTagLength;
|
|
ExtractDicomSummary(target_, *sequence->getItem(sequenceIndex_), 0, ignoreTagLength);
|
|
found_ = true;
|
|
}
|
|
}
|
|
|
|
bool HasFound() const
|
|
{
|
|
return found_;
|
|
}
|
|
};
|
|
|
|
Visitor visitor(target, sequenceIndex);
|
|
IDicomPathVisitor::Apply(visitor, dataset, path);
|
|
return visitor.HasFound();
|
|
}
|
|
|
|
|
|
bool FromDcmtkBridge::LookupStringValue(std::string& target,
|
|
DcmDataset& dataset,
|
|
const DicomTag& key)
|
|
{
|
|
DcmTagKey dcmkey(key.GetGroup(), key.GetElement());
|
|
|
|
const char* str = NULL;
|
|
const Uint8* data = NULL;
|
|
unsigned long size = 0;
|
|
|
|
if (dataset.findAndGetString(dcmkey, str).good() &&
|
|
str != NULL)
|
|
{
|
|
target.assign(str);
|
|
return true;
|
|
}
|
|
else if (dataset.findAndGetUint8Array(dcmkey, data, &size).good() &&
|
|
data != NULL &&
|
|
size > 0)
|
|
{
|
|
/**
|
|
* This special case is necessary for borderline DICOM files
|
|
* that have DICOM tags have the "UN" value representation. New
|
|
* in Orthanc 1.10.1.
|
|
* https://groups.google.com/g/orthanc-users/c/86fobx3ZyoM/m/KBG17un6AQAJ
|
|
**/
|
|
unsigned long l = 0;
|
|
while (l < size &&
|
|
data[l] != 0)
|
|
{
|
|
l++;
|
|
}
|
|
|
|
target.assign(reinterpret_cast<const char*>(data), l);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#include "./FromDcmtkBridge_TransferSyntaxes.impl.h"
|