1902 lines
62 KiB
C++
1902 lines
62 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"
|
|
#include "DicomModification.h"
|
|
|
|
#include "../Compatibility.h"
|
|
#include "../Logging.h"
|
|
#include "../OrthancException.h"
|
|
#include "../SerializationToolbox.h"
|
|
#include "FromDcmtkBridge.h"
|
|
#include "ITagVisitor.h"
|
|
|
|
#include <memory> // For std::unique_ptr
|
|
|
|
|
|
static const std::string ORTHANC_DEIDENTIFICATION_METHOD_2008 =
|
|
"Orthanc " ORTHANC_VERSION " - PS 3.15-2008 Table E.1-1";
|
|
|
|
static const std::string ORTHANC_DEIDENTIFICATION_METHOD_2017c =
|
|
"Orthanc " ORTHANC_VERSION " - PS 3.15-2017c Table E.1-1 Basic Profile";
|
|
|
|
static const std::string ORTHANC_DEIDENTIFICATION_METHOD_2021b =
|
|
"Orthanc " ORTHANC_VERSION " - PS 3.15-2021b Table E.1-1 Basic Profile";
|
|
|
|
static const std::string ORTHANC_DEIDENTIFICATION_METHOD_2023b =
|
|
"Orthanc " ORTHANC_VERSION " - PS 3.15-2023b Table E.1-1 Basic Profile";
|
|
|
|
static const std::string ORTHANC_UNSAFE_DEIDENTIFICATION =
|
|
"Orthanc " ORTHANC_VERSION;
|
|
|
|
namespace Orthanc
|
|
{
|
|
namespace
|
|
{
|
|
enum TagOperation
|
|
{
|
|
TagOperation_Keep,
|
|
TagOperation_Remove
|
|
};
|
|
}
|
|
|
|
|
|
DicomModification::DicomTagRange::DicomTagRange(uint16_t groupFrom,
|
|
uint16_t groupTo,
|
|
uint16_t elementFrom,
|
|
uint16_t elementTo) :
|
|
groupFrom_(groupFrom),
|
|
groupTo_(groupTo),
|
|
elementFrom_(elementFrom),
|
|
elementTo_(elementTo)
|
|
{
|
|
}
|
|
|
|
|
|
bool DicomModification::DicomTagRange::Contains(const DicomTag& tag) const
|
|
{
|
|
return (tag.GetGroup() >= groupFrom_ &&
|
|
tag.GetGroup() <= groupTo_ &&
|
|
tag.GetElement() >= elementFrom_ &&
|
|
tag.GetElement() <= elementTo_);
|
|
}
|
|
|
|
|
|
class DicomModification::RelationshipsVisitor : public ITagVisitor
|
|
{
|
|
private:
|
|
DicomModification& that_;
|
|
|
|
// This method is only applicable to first-level tags
|
|
bool IsManuallyModified(const DicomTag& tag) const
|
|
{
|
|
return (that_.IsCleared(tag) ||
|
|
that_.IsRemoved(tag) ||
|
|
that_.IsReplaced(tag));
|
|
}
|
|
|
|
bool IsKeptSequence(const std::vector<DicomTag>& parentTags,
|
|
const std::vector<size_t>& parentIndexes,
|
|
const DicomTag& tag)
|
|
{
|
|
for (DicomModification::ListOfPaths::const_iterator
|
|
it = that_.keepSequences_.begin(); it != that_.keepSequences_.end(); ++it)
|
|
{
|
|
if (DicomPath::IsMatch(*it, parentTags, parentIndexes, tag))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
Action GetDefaultAction(const std::vector<DicomTag>& parentTags,
|
|
const std::vector<size_t>& parentIndexes,
|
|
const DicomTag& tag)
|
|
{
|
|
if (parentTags.empty() ||
|
|
!that_.isAnonymization_)
|
|
{
|
|
// Don't interfere with first-level tags or with modification
|
|
return Action_None;
|
|
}
|
|
else if (IsKeptSequence(parentTags, parentIndexes, tag))
|
|
{
|
|
return Action_None;
|
|
}
|
|
else if (that_.ArePrivateTagsRemoved() &&
|
|
tag.IsPrivate())
|
|
{
|
|
// New in Orthanc 1.9.5
|
|
// https://groups.google.com/g/orthanc-users/c/l1mcYCC2u-k/m/jOdGYuagAgAJ
|
|
return Action_Remove;
|
|
}
|
|
else if (that_.IsCleared(tag) ||
|
|
that_.IsRemoved(tag))
|
|
{
|
|
// New in Orthanc 1.9.5
|
|
// https://groups.google.com/g/orthanc-users/c/l1mcYCC2u-k/m/jOdGYuagAgAJ
|
|
return Action_Remove;
|
|
}
|
|
else
|
|
{
|
|
return Action_None;
|
|
}
|
|
}
|
|
|
|
public:
|
|
explicit RelationshipsVisitor(DicomModification& that) :
|
|
that_(that)
|
|
{
|
|
}
|
|
|
|
virtual Action VisitNotSupported(const std::vector<DicomTag>& parentTags,
|
|
const std::vector<size_t>& parentIndexes,
|
|
const DicomTag& tag,
|
|
ValueRepresentation vr) ORTHANC_OVERRIDE
|
|
{
|
|
return GetDefaultAction(parentTags, parentIndexes, tag);
|
|
}
|
|
|
|
virtual Action VisitSequence(const std::vector<DicomTag>& parentTags,
|
|
const std::vector<size_t>& parentIndexes,
|
|
const DicomTag& tag,
|
|
size_t countItems) ORTHANC_OVERRIDE
|
|
{
|
|
return GetDefaultAction(parentTags, parentIndexes, tag);
|
|
}
|
|
|
|
virtual Action VisitBinary(const std::vector<DicomTag>& parentTags,
|
|
const std::vector<size_t>& parentIndexes,
|
|
const DicomTag& tag,
|
|
ValueRepresentation vr,
|
|
const void* data,
|
|
size_t size) ORTHANC_OVERRIDE
|
|
{
|
|
return GetDefaultAction(parentTags, parentIndexes, tag);
|
|
}
|
|
|
|
virtual Action VisitIntegers(const std::vector<DicomTag>& parentTags,
|
|
const std::vector<size_t>& parentIndexes,
|
|
const DicomTag& tag,
|
|
ValueRepresentation vr,
|
|
const std::vector<int64_t>& values) ORTHANC_OVERRIDE
|
|
{
|
|
return GetDefaultAction(parentTags, parentIndexes, tag);
|
|
}
|
|
|
|
virtual Action VisitDoubles(const std::vector<DicomTag>& parentTags,
|
|
const std::vector<size_t>& parentIndexes,
|
|
const DicomTag& tag,
|
|
ValueRepresentation vr,
|
|
const std::vector<double>& value) ORTHANC_OVERRIDE
|
|
{
|
|
return GetDefaultAction(parentTags, parentIndexes, tag);
|
|
}
|
|
|
|
virtual Action VisitAttributes(const std::vector<DicomTag>& parentTags,
|
|
const std::vector<size_t>& parentIndexes,
|
|
const DicomTag& tag,
|
|
const std::vector<DicomTag>& value) ORTHANC_OVERRIDE
|
|
{
|
|
return GetDefaultAction(parentTags, parentIndexes, tag);
|
|
}
|
|
|
|
virtual Action VisitString(std::string& newValue,
|
|
const std::vector<DicomTag>& parentTags,
|
|
const std::vector<size_t>& parentIndexes,
|
|
const DicomTag& tag,
|
|
ValueRepresentation vr,
|
|
const std::string& value) ORTHANC_OVERRIDE
|
|
{
|
|
/**
|
|
* Note that all the tags in "uids_" have the VR UI (unique
|
|
* identifier), and are considered as strings.
|
|
*
|
|
* Also, the tags "SOP Instance UID", "Series Instance UID" and
|
|
* "Study Instance UID" are *never* included in "uids_", as they
|
|
* are separately handed by "MapDicomTags()".
|
|
**/
|
|
|
|
assert(that_.uids_.find(DICOM_TAG_STUDY_INSTANCE_UID) == that_.uids_.end());
|
|
assert(that_.uids_.find(DICOM_TAG_SERIES_INSTANCE_UID) == that_.uids_.end());
|
|
assert(that_.uids_.find(DICOM_TAG_SOP_INSTANCE_UID) == that_.uids_.end());
|
|
|
|
if (parentTags.empty())
|
|
{
|
|
// We are on a first-level tag
|
|
if (that_.uids_.find(tag) != that_.uids_.end() &&
|
|
!IsManuallyModified(tag))
|
|
{
|
|
if (tag == DICOM_TAG_PATIENT_ID ||
|
|
tag == DICOM_TAG_PATIENT_NAME)
|
|
{
|
|
assert(vr == ValueRepresentation_LongString ||
|
|
vr == ValueRepresentation_PersonName);
|
|
newValue = that_.MapDicomIdentifier(value, ResourceType_Patient);
|
|
}
|
|
else
|
|
{
|
|
// This is a first-level UID tag that must be anonymized
|
|
assert(vr == ValueRepresentation_UniqueIdentifier ||
|
|
vr == ValueRepresentation_NotSupported /* for older versions of DCMTK */);
|
|
newValue = that_.MapDicomIdentifier(value, ResourceType_Instance);
|
|
}
|
|
|
|
return Action_Replace;
|
|
}
|
|
else
|
|
{
|
|
return Action_None;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// We are within a sequence
|
|
|
|
if (IsKeptSequence(parentTags, parentIndexes, tag))
|
|
{
|
|
// New in Orthanc 1.9.4 - Solves issue LSD-629
|
|
return Action_None;
|
|
}
|
|
|
|
if (that_.isAnonymization_)
|
|
{
|
|
// New in Orthanc 1.9.5, similar to "GetDefaultAction()"
|
|
// https://groups.google.com/g/orthanc-users/c/l1mcYCC2u-k/m/jOdGYuagAgAJ
|
|
if (that_.ArePrivateTagsRemoved() &&
|
|
tag.IsPrivate())
|
|
{
|
|
return Action_Remove;
|
|
}
|
|
else if (that_.IsRemoved(tag))
|
|
{
|
|
return Action_Remove;
|
|
}
|
|
else if (that_.IsCleared(tag))
|
|
{
|
|
// This is different from "GetDefaultAction()", because we know how to clear string tags
|
|
newValue.clear();
|
|
return Action_Replace;
|
|
}
|
|
}
|
|
|
|
if (tag == DICOM_TAG_STUDY_INSTANCE_UID)
|
|
{
|
|
newValue = that_.MapDicomIdentifier(value, ResourceType_Study);
|
|
return Action_Replace;
|
|
}
|
|
else if (tag == DICOM_TAG_SERIES_INSTANCE_UID)
|
|
{
|
|
newValue = that_.MapDicomIdentifier(value, ResourceType_Series);
|
|
return Action_Replace;
|
|
}
|
|
else if (tag == DICOM_TAG_SOP_INSTANCE_UID)
|
|
{
|
|
newValue = that_.MapDicomIdentifier(value, ResourceType_Instance);
|
|
return Action_Replace;
|
|
}
|
|
else if (that_.uids_.find(tag) != that_.uids_.end())
|
|
{
|
|
if (tag == DICOM_TAG_PATIENT_ID ||
|
|
tag == DICOM_TAG_PATIENT_NAME)
|
|
{
|
|
newValue = that_.MapDicomIdentifier(value, ResourceType_Patient);
|
|
}
|
|
else
|
|
{
|
|
assert(vr == ValueRepresentation_UniqueIdentifier ||
|
|
vr == ValueRepresentation_NotSupported /* for older versions of DCMTK */);
|
|
|
|
if (parentTags.size() == 2 &&
|
|
parentTags[0] == DICOM_TAG_REFERENCED_FRAME_OF_REFERENCE_SEQUENCE &&
|
|
parentTags[1] == DICOM_TAG_RT_REFERENCED_STUDY_SEQUENCE &&
|
|
tag == DICOM_TAG_REFERENCED_SOP_INSTANCE_UID)
|
|
{
|
|
/**
|
|
* In RT-STRUCT, this ReferencedSOPInstanceUID is actually
|
|
* referencing a StudyInstanceUID !! (observed in many
|
|
* data sets including:
|
|
* https://wiki.cancerimagingarchive.net/display/Public/Lung+CT+Segmentation+Challenge+2017)
|
|
* Tested in "test_anonymize_relationships_5". Introduced
|
|
* in: https://orthanc.uclouvain.be/hg/orthanc/rev/3513
|
|
**/
|
|
newValue = that_.MapDicomIdentifier(value, ResourceType_Study);
|
|
}
|
|
else
|
|
{
|
|
newValue = that_.MapDicomIdentifier(value, ResourceType_Instance);
|
|
}
|
|
}
|
|
|
|
return Action_Replace;
|
|
}
|
|
else
|
|
{
|
|
return Action_None;
|
|
}
|
|
}
|
|
}
|
|
|
|
void RemoveRelationships(ParsedDicomFile& dicom) const
|
|
{
|
|
for (SetOfTags::const_iterator it = that_.uids_.begin(); it != that_.uids_.end(); ++it)
|
|
{
|
|
assert(*it != DICOM_TAG_STUDY_INSTANCE_UID &&
|
|
*it != DICOM_TAG_SERIES_INSTANCE_UID &&
|
|
*it != DICOM_TAG_SOP_INSTANCE_UID);
|
|
|
|
if (!IsManuallyModified(*it))
|
|
{
|
|
dicom.Remove(*it);
|
|
}
|
|
}
|
|
|
|
// The only two sequences with to the "X/Z/U*" rule in the
|
|
// basic profile. They were already present in Orthanc 1.9.3.
|
|
if (!IsManuallyModified(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE))
|
|
{
|
|
dicom.Remove(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE);
|
|
}
|
|
|
|
if (!IsManuallyModified(DICOM_TAG_SOURCE_IMAGE_SEQUENCE))
|
|
{
|
|
dicom.Remove(DICOM_TAG_SOURCE_IMAGE_SEQUENCE);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
void DicomModification::CancelReplacement(const DicomTag& tag)
|
|
{
|
|
Replacements::iterator it = replacements_.find(tag);
|
|
|
|
if (it != replacements_.end())
|
|
{
|
|
assert(it->second != NULL);
|
|
delete it->second;
|
|
replacements_.erase(it);
|
|
}
|
|
}
|
|
|
|
|
|
void DicomModification::ReplaceInternal(const DicomTag& tag,
|
|
const Json::Value& value)
|
|
{
|
|
Replacements::iterator it = replacements_.find(tag);
|
|
|
|
if (it != replacements_.end())
|
|
{
|
|
assert(it->second != NULL);
|
|
delete it->second;
|
|
it->second = NULL; // In the case of an exception during the clone
|
|
it->second = new Json::Value(value); // Clone
|
|
}
|
|
else
|
|
{
|
|
replacements_[tag] = new Json::Value(value); // Clone
|
|
}
|
|
}
|
|
|
|
|
|
void DicomModification::ClearReplacements()
|
|
{
|
|
for (Replacements::iterator it = replacements_.begin();
|
|
it != replacements_.end(); ++it)
|
|
{
|
|
assert(it->second != NULL);
|
|
delete it->second;
|
|
}
|
|
|
|
replacements_.clear();
|
|
|
|
for (SequenceReplacements::iterator it = sequenceReplacements_.begin();
|
|
it != sequenceReplacements_.end(); ++it)
|
|
{
|
|
assert(*it != NULL);
|
|
assert((*it)->GetPath().GetPrefixLength() > 0);
|
|
delete *it;
|
|
}
|
|
|
|
sequenceReplacements_.clear();
|
|
}
|
|
|
|
|
|
void DicomModification::MarkNotOrthancAnonymization()
|
|
{
|
|
Replacements::iterator it = replacements_.find(DICOM_TAG_DEIDENTIFICATION_METHOD);
|
|
|
|
if (it != replacements_.end())
|
|
{
|
|
assert(it->second != NULL);
|
|
|
|
if (it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2008 ||
|
|
it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2017c ||
|
|
it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2021b ||
|
|
it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2023b)
|
|
{
|
|
ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_UNSAFE_DEIDENTIFICATION);
|
|
}
|
|
}
|
|
}
|
|
|
|
void DicomModification::RegisterMappedDicomIdentifier(const std::string& original,
|
|
const std::string& mapped,
|
|
ResourceType level)
|
|
{
|
|
UidMap::const_iterator previous = uidMap_.find(std::make_pair(level, original));
|
|
|
|
if (previous == uidMap_.end())
|
|
{
|
|
uidMap_.insert(std::make_pair(std::make_pair(level, original), mapped));
|
|
}
|
|
}
|
|
|
|
std::string DicomModification::MapDicomIdentifier(const std::string& original,
|
|
ResourceType level)
|
|
{
|
|
const std::string stripped = Toolbox::StripSpaces(original);
|
|
|
|
std::string mapped;
|
|
|
|
UidMap::const_iterator previous = uidMap_.find(std::make_pair(level, stripped));
|
|
|
|
if (previous == uidMap_.end())
|
|
{
|
|
if (identifierGenerator_ == NULL)
|
|
{
|
|
mapped = FromDcmtkBridge::GenerateUniqueIdentifier(level);
|
|
}
|
|
else
|
|
{
|
|
if (!identifierGenerator_->Apply(mapped, stripped, level, currentSource_))
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError,
|
|
"Unable to generate an anonymized ID");
|
|
}
|
|
}
|
|
|
|
uidMap_.insert(std::make_pair(std::make_pair(level, stripped), mapped));
|
|
}
|
|
else
|
|
{
|
|
mapped = previous->second;
|
|
}
|
|
|
|
return mapped;
|
|
}
|
|
|
|
|
|
void DicomModification::MapDicomTags(ParsedDicomFile& dicom,
|
|
ResourceType level)
|
|
{
|
|
std::unique_ptr<DicomTag> tag;
|
|
|
|
switch (level)
|
|
{
|
|
case ResourceType_Study:
|
|
tag.reset(new DicomTag(DICOM_TAG_STUDY_INSTANCE_UID));
|
|
break;
|
|
|
|
case ResourceType_Series:
|
|
tag.reset(new DicomTag(DICOM_TAG_SERIES_INSTANCE_UID));
|
|
break;
|
|
|
|
case ResourceType_Instance:
|
|
tag.reset(new DicomTag(DICOM_TAG_SOP_INSTANCE_UID));
|
|
break;
|
|
|
|
default:
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
|
|
std::string original;
|
|
if (!const_cast<const ParsedDicomFile&>(dicom).GetTagValue(original, *tag))
|
|
{
|
|
original = "";
|
|
}
|
|
|
|
std::string mapped = MapDicomIdentifier(original, level);
|
|
|
|
dicom.Replace(*tag, mapped,
|
|
false /* don't try and decode data URI scheme for UIDs */,
|
|
DicomReplaceMode_InsertIfAbsent, privateCreator_);
|
|
}
|
|
|
|
|
|
DicomModification::DicomModification() :
|
|
removePrivateTags_(false),
|
|
keepLabels_(false),
|
|
level_(ResourceType_Instance),
|
|
allowManualIdentifiers_(true),
|
|
keepStudyInstanceUid_(false),
|
|
keepSeriesInstanceUid_(false),
|
|
keepSopInstanceUid_(false),
|
|
updateReferencedRelationships_(true),
|
|
isAnonymization_(false),
|
|
//privateCreator_("PrivateCreator"),
|
|
identifierGenerator_(NULL)
|
|
{
|
|
}
|
|
|
|
DicomModification::~DicomModification()
|
|
{
|
|
ClearReplacements();
|
|
}
|
|
|
|
void DicomModification::Keep(const DicomTag& tag)
|
|
{
|
|
keep_.insert(tag);
|
|
removals_.erase(tag);
|
|
clearings_.erase(tag);
|
|
uids_.erase(tag);
|
|
|
|
CancelReplacement(tag);
|
|
|
|
if (tag == DICOM_TAG_STUDY_INSTANCE_UID)
|
|
{
|
|
keepStudyInstanceUid_ = true;
|
|
}
|
|
else if (tag == DICOM_TAG_SERIES_INSTANCE_UID)
|
|
{
|
|
keepSeriesInstanceUid_ = true;
|
|
}
|
|
else if (tag == DICOM_TAG_SOP_INSTANCE_UID)
|
|
{
|
|
keepSopInstanceUid_ = true;
|
|
}
|
|
else if (tag.IsPrivate())
|
|
{
|
|
privateTagsToKeep_.insert(tag);
|
|
}
|
|
|
|
MarkNotOrthancAnonymization();
|
|
}
|
|
|
|
void DicomModification::Remove(const DicomTag& tag)
|
|
{
|
|
removals_.insert(tag);
|
|
clearings_.erase(tag);
|
|
uids_.erase(tag);
|
|
CancelReplacement(tag);
|
|
privateTagsToKeep_.erase(tag);
|
|
|
|
MarkNotOrthancAnonymization();
|
|
}
|
|
|
|
void DicomModification::Clear(const DicomTag& tag)
|
|
{
|
|
removals_.erase(tag);
|
|
clearings_.insert(tag);
|
|
uids_.erase(tag);
|
|
CancelReplacement(tag);
|
|
privateTagsToKeep_.erase(tag);
|
|
|
|
MarkNotOrthancAnonymization();
|
|
}
|
|
|
|
bool DicomModification::IsRemoved(const DicomTag& tag) const
|
|
{
|
|
if (removals_.find(tag) != removals_.end())
|
|
{
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
for (RemovedRanges::const_iterator it = removedRanges_.begin();
|
|
it != removedRanges_.end(); ++it)
|
|
{
|
|
if (it->Contains(tag))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool DicomModification::IsCleared(const DicomTag& tag) const
|
|
{
|
|
return clearings_.find(tag) != clearings_.end();
|
|
}
|
|
|
|
void DicomModification::Replace(const DicomTag& tag,
|
|
const Json::Value& value,
|
|
bool safeForAnonymization)
|
|
{
|
|
clearings_.erase(tag);
|
|
removals_.erase(tag);
|
|
uids_.erase(tag);
|
|
privateTagsToKeep_.erase(tag);
|
|
ReplaceInternal(tag, value);
|
|
|
|
if (!safeForAnonymization)
|
|
{
|
|
MarkNotOrthancAnonymization();
|
|
}
|
|
}
|
|
|
|
|
|
bool DicomModification::IsReplaced(const DicomTag& tag) const
|
|
{
|
|
return replacements_.find(tag) != replacements_.end();
|
|
}
|
|
|
|
bool DicomModification::IsKept(const DicomTag& tag) const
|
|
{
|
|
return keep_.find(tag) != keep_.end();
|
|
}
|
|
|
|
const Json::Value& DicomModification::GetReplacement(const DicomTag& tag) const
|
|
{
|
|
Replacements::const_iterator it = replacements_.find(tag);
|
|
|
|
if (it == replacements_.end())
|
|
{
|
|
throw OrthancException(ErrorCode_InexistentItem);
|
|
}
|
|
else
|
|
{
|
|
assert(it->second != NULL);
|
|
return *it->second;
|
|
}
|
|
}
|
|
|
|
|
|
std::string DicomModification::GetReplacementAsString(const DicomTag& tag) const
|
|
{
|
|
const Json::Value& json = GetReplacement(tag);
|
|
|
|
if (json.type() != Json::stringValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadParameterType);
|
|
}
|
|
else
|
|
{
|
|
return json.asString();
|
|
}
|
|
}
|
|
|
|
|
|
void DicomModification::SetRemovePrivateTags(bool removed)
|
|
{
|
|
removePrivateTags_ = removed;
|
|
|
|
if (!removed)
|
|
{
|
|
MarkNotOrthancAnonymization();
|
|
}
|
|
}
|
|
|
|
bool DicomModification::ArePrivateTagsRemoved() const
|
|
{
|
|
return removePrivateTags_;
|
|
}
|
|
|
|
void DicomModification::SetKeepLabels(bool keep)
|
|
{
|
|
keepLabels_ = keep;
|
|
}
|
|
|
|
bool DicomModification::AreLabelsKept() const
|
|
{
|
|
return keepLabels_;
|
|
}
|
|
|
|
void DicomModification::SetLevel(ResourceType level)
|
|
{
|
|
uidMap_.clear();
|
|
level_ = level;
|
|
|
|
if (level != ResourceType_Patient)
|
|
{
|
|
MarkNotOrthancAnonymization();
|
|
}
|
|
}
|
|
|
|
ResourceType DicomModification::GetLevel() const
|
|
{
|
|
return level_;
|
|
}
|
|
|
|
|
|
static void SetupUidsFromOrthancInternal(std::set<DicomTag>& uids,
|
|
std::set<DicomTag>& removals,
|
|
const DicomTag& tag)
|
|
{
|
|
uids.insert(tag);
|
|
removals.erase(tag); // Necessary if unserializing a job from 1.9.3
|
|
}
|
|
|
|
|
|
void DicomModification::SetupUidsFromOrthanc_1_9_3()
|
|
{
|
|
/**
|
|
* Values below come from the hardcoded UID of Orthanc 1.9.3
|
|
* in DicomModification::RelationshipsVisitor::VisitString() and
|
|
* DicomModification::RelationshipsVisitor::RemoveRelationships()
|
|
* https://orthanc.uclouvain.be/hg/orthanc/file/Orthanc-1.9.3/OrthancFramework/Sources/DicomParsing/DicomModification.cpp#l117
|
|
**/
|
|
uids_.clear();
|
|
|
|
// (*) "PatientID" and "PatientName" are handled as UIDs since Orthanc 1.9.4
|
|
uids_.insert(DICOM_TAG_PATIENT_ID);
|
|
uids_.insert(DICOM_TAG_PATIENT_NAME);
|
|
|
|
SetupUidsFromOrthancInternal(uids_, removals_, DicomTag(0x0008, 0x0014)); // Instance Creator UID <= from SetupAnonymization2008()
|
|
SetupUidsFromOrthancInternal(uids_, removals_, DicomTag(0x0008, 0x1155)); // Referenced SOP Instance UID <= from VisitString() + RemoveRelationships()
|
|
SetupUidsFromOrthancInternal(uids_, removals_, DicomTag(0x0020, 0x0052)); // Frame of Reference UID <= from VisitString() + RemoveRelationships()
|
|
SetupUidsFromOrthancInternal(uids_, removals_, DicomTag(0x0020, 0x0200)); // Synchronization Frame of Reference UID <= from SetupAnonymization2008()
|
|
SetupUidsFromOrthancInternal(uids_, removals_, DicomTag(0x0040, 0xa124)); // UID <= from SetupAnonymization2008()
|
|
SetupUidsFromOrthancInternal(uids_, removals_, DicomTag(0x0088, 0x0140)); // Storage Media File-set UID <= from SetupAnonymization2008()
|
|
SetupUidsFromOrthancInternal(uids_, removals_, DicomTag(0x3006, 0x0024)); // Referenced Frame of Reference UID <= from VisitString() + RemoveRelationships()
|
|
SetupUidsFromOrthancInternal(uids_, removals_, DicomTag(0x3006, 0x00c2)); // Related Frame of Reference UID <= from VisitString() + RemoveRelationships()
|
|
}
|
|
|
|
|
|
void DicomModification::SetupAnonymization2008()
|
|
{
|
|
// This is Table E.1-1 from PS 3.15-2008 - DICOM Part 15: Security and System Management Profiles
|
|
// https://raw.githubusercontent.com/jodogne/dicom-specification/master/2008/08_15pu.pdf
|
|
|
|
SetupUidsFromOrthanc_1_9_3();
|
|
|
|
//uids_.insert(DicomTag(0x0008, 0x0014)); // Instance Creator UID => set in SetupUidsFromOrthanc_1_9_3()
|
|
//removals_.insert(DicomTag(0x0008, 0x0018)); // SOP Instance UID => set in Apply()
|
|
removals_.insert(DicomTag(0x0008, 0x0050)); // Accession Number
|
|
removals_.insert(DicomTag(0x0008, 0x0080)); // Institution Name
|
|
removals_.insert(DicomTag(0x0008, 0x0081)); // Institution Address
|
|
removals_.insert(DicomTag(0x0008, 0x0090)); // Referring Physician's Name
|
|
removals_.insert(DicomTag(0x0008, 0x0092)); // Referring Physician's Address
|
|
removals_.insert(DicomTag(0x0008, 0x0094)); // Referring Physician's Telephone Numbers
|
|
removals_.insert(DicomTag(0x0008, 0x1010)); // Station Name
|
|
removals_.insert(DicomTag(0x0008, 0x1030)); // Study Description
|
|
removals_.insert(DicomTag(0x0008, 0x103e)); // Series Description
|
|
removals_.insert(DicomTag(0x0008, 0x1040)); // Institutional Department Name
|
|
removals_.insert(DicomTag(0x0008, 0x1048)); // Physician(s) of Record
|
|
removals_.insert(DicomTag(0x0008, 0x1050)); // Performing Physicians' Name
|
|
removals_.insert(DicomTag(0x0008, 0x1060)); // Name of Physician(s) Reading Study
|
|
removals_.insert(DicomTag(0x0008, 0x1070)); // Operators' Name
|
|
removals_.insert(DicomTag(0x0008, 0x1080)); // Admitting Diagnoses Description
|
|
//uids_.insert(DicomTag(0x0008, 0x1155)); // Referenced SOP Instance UID => set in SetupUidsFromOrthanc_1_9_3()
|
|
removals_.insert(DicomTag(0x0008, 0x2111)); // Derivation Description
|
|
//removals_.insert(DicomTag(0x0010, 0x0010)); // Patient's Name => cf. below (*)
|
|
//removals_.insert(DicomTag(0x0010, 0x0020)); // Patient ID => cf. below (*)
|
|
removals_.insert(DicomTag(0x0010, 0x0030)); // Patient's Birth Date
|
|
removals_.insert(DicomTag(0x0010, 0x0032)); // Patient's Birth Time
|
|
removals_.insert(DicomTag(0x0010, 0x0040)); // Patient's Sex
|
|
removals_.insert(DicomTag(0x0010, 0x1000)); // Other Patient Ids
|
|
removals_.insert(DicomTag(0x0010, 0x1001)); // Other Patient Names
|
|
removals_.insert(DicomTag(0x0010, 0x1010)); // Patient's Age
|
|
removals_.insert(DicomTag(0x0010, 0x1020)); // Patient's Size
|
|
removals_.insert(DicomTag(0x0010, 0x1030)); // Patient's Weight
|
|
removals_.insert(DicomTag(0x0010, 0x1090)); // Medical Record Locator
|
|
removals_.insert(DicomTag(0x0010, 0x2160)); // Ethnic Group
|
|
removals_.insert(DicomTag(0x0010, 0x2180)); // Occupation
|
|
removals_.insert(DicomTag(0x0010, 0x21b0)); // Additional Patient's History
|
|
removals_.insert(DicomTag(0x0010, 0x4000)); // Patient Comments
|
|
removals_.insert(DicomTag(0x0018, 0x1000)); // Device Serial Number
|
|
removals_.insert(DicomTag(0x0018, 0x1030)); // Protocol Name
|
|
//removals_.insert(DicomTag(0x0020, 0x000d)); // Study Instance UID => set in Apply()
|
|
//removals_.insert(DicomTag(0x0020, 0x000e)); // Series Instance UID => set in Apply()
|
|
removals_.insert(DicomTag(0x0020, 0x0010)); // Study ID
|
|
//uids_.insert(DicomTag(0x0020, 0x0052)); // Frame of Reference UID => set in SetupUidsFromOrthanc_1_9_3()
|
|
//uids_.insert(DicomTag(0x0020, 0x0200)); // Synchronization Frame of Reference UID => set in SetupUidsFromOrthanc_1_9_3()
|
|
removals_.insert(DicomTag(0x0020, 0x4000)); // Image Comments
|
|
removals_.insert(DicomTag(0x0040, 0x0275)); // Request Attributes Sequence
|
|
//uids_.insert(DicomTag(0x0040, 0xa124)); // UID => set in SetupUidsFromOrthanc_1_9_3()
|
|
removals_.insert(DicomTag(0x0040, 0xa730)); // Content Sequence
|
|
//uids_.insert(DicomTag(0x0088, 0x0140)); // Storage Media File-set UID => set in SetupUidsFromOrthanc_1_9_3()
|
|
//uids_.insert(DicomTag(0x3006, 0x0024)); // Referenced Frame of Reference UID => set in SetupUidsFromOrthanc_1_9_3()
|
|
//uids_.insert(DicomTag(0x3006, 0x00c2)); // Related Frame of Reference UID => set in SetupUidsFromOrthanc_1_9_3()
|
|
|
|
// Some more removals (from the experience of DICOM files at the CHU of Liege)
|
|
removals_.insert(DicomTag(0x0010, 0x1040)); // Patient's Address
|
|
removals_.insert(DicomTag(0x0032, 0x1032)); // Requesting Physician
|
|
removals_.insert(DicomTag(0x0010, 0x2154)); // PatientTelephoneNumbers
|
|
removals_.insert(DicomTag(0x0010, 0x2000)); // Medical Alerts
|
|
|
|
// Set the DeidentificationMethod tag
|
|
ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD_2008);
|
|
}
|
|
|
|
|
|
void DicomModification::SetupAnonymization2017c()
|
|
{
|
|
/**
|
|
* This is Table E.1-1 from PS 3.15-2017c (DICOM Part 15: Security
|
|
* and System Management Profiles), "basic profile" column. It was
|
|
* generated automatically by calling:
|
|
* "../../../OrthancServer/Resources/GenerateAnonymizationProfile.py
|
|
* https://raw.githubusercontent.com/jodogne/dicom-specification/master/2017c/part15.xml"
|
|
**/
|
|
|
|
#include "DicomModification_Anonymization2017c.impl.h"
|
|
|
|
// Set the DeidentificationMethod tag
|
|
ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD_2017c);
|
|
}
|
|
|
|
|
|
void DicomModification::SetupAnonymization2021b()
|
|
{
|
|
/**
|
|
* This is Table E.1-1 from PS 3.15-2021b (DICOM Part 15: Security
|
|
* and System Management Profiles), "basic profile" column. It was
|
|
* generated automatically by calling:
|
|
* "../../../OrthancServer/Resources/GenerateAnonymizationProfile.py
|
|
* https://raw.githubusercontent.com/jodogne/dicom-specification/master/2021b/part15.xml"
|
|
**/
|
|
|
|
#include "DicomModification_Anonymization2021b.impl.h"
|
|
|
|
// Set the DeidentificationMethod tag
|
|
ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD_2021b);
|
|
}
|
|
|
|
|
|
void DicomModification::SetupAnonymization2023b()
|
|
{
|
|
/**
|
|
* This is Table E.1-1 from PS 3.15-2023b (DICOM Part 15: Security
|
|
* and System Management Profiles), "basic profile" column. It was
|
|
* generated automatically by calling:
|
|
* "../../../OrthancServer/Resources/GenerateAnonymizationProfile.py
|
|
* https://raw.githubusercontent.com/jodogne/dicom-specification/master/2023b/part15.xml"
|
|
*
|
|
* http://dicom.nema.org/medical/dicom/current/output/chtml/part15/chapter_E.html#table_E.1-1a
|
|
* http://dicom.nema.org/medical/dicom/current/output/chtml/part15/chapter_E.html#table_E.1-1
|
|
**/
|
|
|
|
#include "DicomModification_Anonymization2023b.impl.h"
|
|
|
|
// Set the DeidentificationMethod tag
|
|
ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD_2023b);
|
|
}
|
|
|
|
|
|
void DicomModification::SetupAnonymization(DicomVersion version)
|
|
{
|
|
isAnonymization_ = true;
|
|
|
|
keep_.clear();
|
|
removals_.clear();
|
|
clearings_.clear();
|
|
removedRanges_.clear();
|
|
uids_.clear();
|
|
ClearReplacements();
|
|
removePrivateTags_ = true;
|
|
level_ = ResourceType_Patient;
|
|
uidMap_.clear();
|
|
privateTagsToKeep_.clear();
|
|
keepSequences_.clear();
|
|
removeSequences_.clear();
|
|
|
|
switch (version)
|
|
{
|
|
case DicomVersion_2008:
|
|
SetupAnonymization2008();
|
|
break;
|
|
|
|
case DicomVersion_2017c:
|
|
SetupAnonymization2017c();
|
|
break;
|
|
|
|
case DicomVersion_2021b:
|
|
SetupAnonymization2021b();
|
|
break;
|
|
|
|
case DicomVersion_2023b:
|
|
SetupAnonymization2023b();
|
|
break;
|
|
|
|
default:
|
|
throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
}
|
|
|
|
// Set the PatientIdentityRemoved tag
|
|
ReplaceInternal(DICOM_TAG_PATIENT_IDENTITY_REMOVED, "YES");
|
|
|
|
// (*) Choose a random patient name and ID
|
|
uids_.insert(DICOM_TAG_PATIENT_ID);
|
|
uids_.insert(DICOM_TAG_PATIENT_NAME);
|
|
|
|
// Sanity check
|
|
for (SetOfTags::const_iterator it = uids_.begin(); it != uids_.end(); ++it)
|
|
{
|
|
ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(*it);
|
|
if (*it == DICOM_TAG_PATIENT_ID)
|
|
{
|
|
if (vr != ValueRepresentation_LongString &&
|
|
vr != ValueRepresentation_NotSupported /* if no dictionary loaded */)
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
}
|
|
else if (*it == DICOM_TAG_PATIENT_NAME)
|
|
{
|
|
if (vr != ValueRepresentation_PersonName &&
|
|
vr != ValueRepresentation_NotSupported /* if no dictionary loaded */)
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
}
|
|
else if (vr != ValueRepresentation_UniqueIdentifier &&
|
|
vr != ValueRepresentation_NotSupported /* for older versions of DCMTK */)
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
}
|
|
}
|
|
|
|
void DicomModification::Apply(ParsedDicomFile& toModify)
|
|
{
|
|
// Check the request
|
|
assert(ResourceType_Patient + 1 == ResourceType_Study &&
|
|
ResourceType_Study + 1 == ResourceType_Series &&
|
|
ResourceType_Series + 1 == ResourceType_Instance);
|
|
|
|
if (IsRemoved(DICOM_TAG_PATIENT_ID) ||
|
|
IsRemoved(DICOM_TAG_STUDY_INSTANCE_UID) ||
|
|
IsRemoved(DICOM_TAG_SERIES_INSTANCE_UID) ||
|
|
IsRemoved(DICOM_TAG_SOP_INSTANCE_UID))
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest, "It is forbidden to remove one of the main Dicom identifiers");
|
|
}
|
|
|
|
if (!allowManualIdentifiers_)
|
|
{
|
|
// Sanity checks at the patient level
|
|
if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest,
|
|
"When modifying a patient, the StudyInstanceUID cannot be manually modified");
|
|
}
|
|
|
|
if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest,
|
|
"When modifying a patient, the SeriesInstanceUID cannot be manually modified");
|
|
}
|
|
|
|
if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest,
|
|
"When modifying a patient, the SopInstanceUID cannot be manually modified");
|
|
}
|
|
|
|
// Sanity checks at the study level
|
|
if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest,
|
|
"When modifying a study, the SeriesInstanceUID cannot be manually modified");
|
|
}
|
|
|
|
if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest,
|
|
"When modifying a study, the SopInstanceUID cannot be manually modified");
|
|
}
|
|
|
|
// Sanity checks at the series level
|
|
if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest,
|
|
"When modifying a series, the SopInstanceUID cannot be manually modified");
|
|
}
|
|
}
|
|
|
|
|
|
// (0) Create a summary of the source file, if a custom generator
|
|
// is provided
|
|
if (identifierGenerator_ != NULL)
|
|
{
|
|
toModify.ExtractDicomSummary(currentSource_, ORTHANC_MAXIMUM_TAG_LENGTH);
|
|
}
|
|
|
|
// (1) Make sure the relationships are updated with the ids that we force too
|
|
// i.e: an RT-STRUCT is referencing its own StudyInstanceUID
|
|
if (isAnonymization_ && updateReferencedRelationships_)
|
|
{
|
|
if (IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
|
|
{
|
|
std::string original;
|
|
std::string replacement = GetReplacementAsString(DICOM_TAG_STUDY_INSTANCE_UID);
|
|
const_cast<const ParsedDicomFile&>(toModify).GetTagValue(original, DICOM_TAG_STUDY_INSTANCE_UID);
|
|
RegisterMappedDicomIdentifier(original, replacement, ResourceType_Study);
|
|
}
|
|
|
|
if (IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
|
|
{
|
|
std::string original;
|
|
std::string replacement = GetReplacementAsString(DICOM_TAG_SERIES_INSTANCE_UID);
|
|
const_cast<const ParsedDicomFile&>(toModify).GetTagValue(original, DICOM_TAG_SERIES_INSTANCE_UID);
|
|
RegisterMappedDicomIdentifier(original, replacement, ResourceType_Series);
|
|
}
|
|
|
|
if (IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
|
|
{
|
|
std::string original;
|
|
std::string replacement = GetReplacementAsString(DICOM_TAG_SOP_INSTANCE_UID);
|
|
const_cast<const ParsedDicomFile&>(toModify).GetTagValue(original, DICOM_TAG_SOP_INSTANCE_UID);
|
|
RegisterMappedDicomIdentifier(original, replacement, ResourceType_Instance);
|
|
}
|
|
}
|
|
|
|
|
|
// (2) Remove the private tags, if need be
|
|
if (removePrivateTags_)
|
|
{
|
|
toModify.RemovePrivateTags(privateTagsToKeep_);
|
|
}
|
|
|
|
// (3) Clear the tags specified by the user
|
|
for (SetOfTags::const_iterator it = clearings_.begin();
|
|
it != clearings_.end(); ++it)
|
|
{
|
|
toModify.Clear(*it, true /* only clear if the tag exists in the original file */);
|
|
}
|
|
|
|
// (4) Remove the tags specified by the user
|
|
for (SetOfTags::const_iterator it = removals_.begin();
|
|
it != removals_.end(); ++it)
|
|
{
|
|
toModify.Remove(*it);
|
|
}
|
|
|
|
// (5) Replace the tags
|
|
for (Replacements::const_iterator it = replacements_.begin();
|
|
it != replacements_.end(); ++it)
|
|
{
|
|
assert(it->second != NULL);
|
|
toModify.Replace(it->first, *it->second, true /* decode data URI scheme */,
|
|
DicomReplaceMode_InsertIfAbsent, privateCreator_);
|
|
}
|
|
|
|
// (6) Update the DICOM identifiers
|
|
if (level_ <= ResourceType_Study &&
|
|
!IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
|
|
{
|
|
if (keepStudyInstanceUid_)
|
|
{
|
|
LOG(WARNING) << "Modifying a study while keeping its original StudyInstanceUID: This should be avoided!";
|
|
}
|
|
else
|
|
{
|
|
MapDicomTags(toModify, ResourceType_Study);
|
|
}
|
|
}
|
|
|
|
if (level_ <= ResourceType_Series &&
|
|
!IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
|
|
{
|
|
if (keepSeriesInstanceUid_)
|
|
{
|
|
LOG(WARNING) << "Modifying a series while keeping its original SeriesInstanceUID: This should be avoided!";
|
|
}
|
|
else
|
|
{
|
|
MapDicomTags(toModify, ResourceType_Series);
|
|
}
|
|
}
|
|
|
|
if (level_ <= ResourceType_Instance && // Always true
|
|
!IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
|
|
{
|
|
if (keepSopInstanceUid_)
|
|
{
|
|
LOG(WARNING) << "Modifying an instance while keeping its original SOPInstanceUID: This should be avoided!";
|
|
}
|
|
else
|
|
{
|
|
MapDicomTags(toModify, ResourceType_Instance);
|
|
}
|
|
}
|
|
|
|
// (7) Update the "referenced" relationships in the case of an anonymization
|
|
if (isAnonymization_)
|
|
{
|
|
RelationshipsVisitor visitor(*this);
|
|
|
|
if (updateReferencedRelationships_)
|
|
{
|
|
const_cast<const ParsedDicomFile&>(toModify).Apply(visitor);
|
|
}
|
|
else
|
|
{
|
|
visitor.RemoveRelationships(toModify);
|
|
}
|
|
}
|
|
|
|
// (8) New in Orthanc 1.9.4: Apply modifications to subsequences
|
|
for (ListOfPaths::const_iterator it = removeSequences_.begin();
|
|
it != removeSequences_.end(); ++it)
|
|
{
|
|
assert(it->GetPrefixLength() > 0);
|
|
toModify.RemovePath(*it);
|
|
}
|
|
|
|
for (SequenceReplacements::const_iterator it = sequenceReplacements_.begin();
|
|
it != sequenceReplacements_.end(); ++it)
|
|
{
|
|
assert(*it != NULL);
|
|
assert((*it)->GetPath().GetPrefixLength() > 0);
|
|
toModify.ReplacePath((*it)->GetPath(), (*it)->GetValue(), true /* decode data URI scheme */,
|
|
DicomReplaceMode_InsertIfAbsent, privateCreator_);
|
|
}
|
|
}
|
|
|
|
void DicomModification::SetAllowManualIdentifiers(bool check)
|
|
{
|
|
allowManualIdentifiers_ = check;
|
|
}
|
|
|
|
bool DicomModification::AreAllowManualIdentifiers() const
|
|
{
|
|
return allowManualIdentifiers_;
|
|
}
|
|
|
|
|
|
static bool IsDatabaseKey(const DicomTag& tag)
|
|
{
|
|
return (tag == DICOM_TAG_PATIENT_ID ||
|
|
tag == DICOM_TAG_STUDY_INSTANCE_UID ||
|
|
tag == DICOM_TAG_SERIES_INSTANCE_UID ||
|
|
tag == DICOM_TAG_SOP_INSTANCE_UID);
|
|
}
|
|
|
|
|
|
static void ParseListOfTags(DicomModification& target,
|
|
const Json::Value& query,
|
|
TagOperation operation,
|
|
bool force)
|
|
{
|
|
if (!query.isArray())
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest);
|
|
}
|
|
|
|
for (Json::Value::ArrayIndex i = 0; i < query.size(); i++)
|
|
{
|
|
if (query[i].type() != Json::stringValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest);
|
|
}
|
|
|
|
std::string name = query[i].asString();
|
|
|
|
const DicomPath path(DicomPath::Parse(name));
|
|
|
|
if (path.GetPrefixLength() == 0 &&
|
|
!force &&
|
|
IsDatabaseKey(path.GetFinalTag()))
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest,
|
|
"Marking tag \"" + name + "\" as to be " +
|
|
(operation == TagOperation_Keep ? "kept" : "removed") +
|
|
" requires the \"Force\" option to be set to true");
|
|
}
|
|
|
|
switch (operation)
|
|
{
|
|
case TagOperation_Keep:
|
|
target.Keep(path);
|
|
LOG(TRACE) << "Keep: " << name << " = " << path.Format();
|
|
break;
|
|
|
|
case TagOperation_Remove:
|
|
target.Remove(path);
|
|
LOG(TRACE) << "Remove: " << name << " = " << path.Format();
|
|
break;
|
|
|
|
default:
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void ParseReplacements(DicomModification& target,
|
|
const Json::Value& replacements,
|
|
bool force)
|
|
{
|
|
if (!replacements.isObject())
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest);
|
|
}
|
|
|
|
Json::Value::Members members = replacements.getMemberNames();
|
|
for (size_t i = 0; i < members.size(); i++)
|
|
{
|
|
const std::string& name = members[i];
|
|
const Json::Value& value = replacements[name];
|
|
|
|
const DicomPath path(DicomPath::Parse(name));
|
|
|
|
if (path.GetPrefixLength() == 0 &&
|
|
!force &&
|
|
IsDatabaseKey(path.GetFinalTag()))
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest,
|
|
"Marking tag \"" + name + "\" as to be replaced " +
|
|
"requires the \"Force\" option to be set to true");
|
|
}
|
|
|
|
target.Replace(path, value, false /* not safe for anonymization */);
|
|
|
|
LOG(TRACE) << "Replace: " << name << " = " << path.Format()
|
|
<< " by: " << value.toStyledString();
|
|
}
|
|
}
|
|
|
|
|
|
static bool GetBooleanValue(const std::string& member,
|
|
const Json::Value& json,
|
|
bool defaultValue)
|
|
{
|
|
if (!json.isMember(member))
|
|
{
|
|
return defaultValue;
|
|
}
|
|
else if (json[member].type() == Json::booleanValue)
|
|
{
|
|
return json[member].asBool();
|
|
}
|
|
else
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat,
|
|
"Member \"" + member + "\" should be a Boolean value");
|
|
}
|
|
}
|
|
|
|
|
|
void DicomModification::ParseModifyRequest(const Json::Value& request)
|
|
{
|
|
if (!request.isObject())
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat);
|
|
}
|
|
|
|
bool force = GetBooleanValue("Force", request, false);
|
|
|
|
if (GetBooleanValue("RemovePrivateTags", request, false))
|
|
{
|
|
SetRemovePrivateTags(true);
|
|
}
|
|
|
|
if (GetBooleanValue("KeepLabels", request, false))
|
|
{
|
|
SetKeepLabels(true);
|
|
}
|
|
|
|
if (request.isMember("Remove"))
|
|
{
|
|
ParseListOfTags(*this, request["Remove"], TagOperation_Remove, force);
|
|
}
|
|
|
|
if (request.isMember("Replace"))
|
|
{
|
|
ParseReplacements(*this, request["Replace"], force);
|
|
}
|
|
|
|
// The "Keep" operation only makes sense for the tags
|
|
// StudyInstanceUID, SeriesInstanceUID and SOPInstanceUID. Avoid
|
|
// this feature as much as possible, as this breaks the DICOM
|
|
// model of the real world, except if you know exactly what
|
|
// you're doing!
|
|
if (request.isMember("Keep"))
|
|
{
|
|
ParseListOfTags(*this, request["Keep"], TagOperation_Keep, force);
|
|
}
|
|
|
|
// New in Orthanc 1.6.0
|
|
if (request.isMember("PrivateCreator"))
|
|
{
|
|
privateCreator_ = SerializationToolbox::ReadString(request, "PrivateCreator");
|
|
}
|
|
|
|
if (!force)
|
|
{
|
|
/**
|
|
* Sanity checks about the manual replacement of DICOM
|
|
* identifiers. Those checks were part of
|
|
* "DicomModification::Apply()" in Orthanc <= 1.11.2, and
|
|
* couldn't be disabled even if using the "Force" flag. Check
|
|
* out:
|
|
* https://groups.google.com/g/orthanc-users/c/xMUUZAnBa5g/m/WCEu-U2NBQAJ
|
|
**/
|
|
bool isReplacedPatientId = (IsReplaced(DICOM_TAG_PATIENT_ID) ||
|
|
uids_.find(DICOM_TAG_PATIENT_ID) != uids_.end());
|
|
|
|
if (level_ == ResourceType_Patient && !isReplacedPatientId)
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest,
|
|
"When modifying a patient, her PatientID is required to be modified.");
|
|
}
|
|
|
|
if (level_ == ResourceType_Study && isReplacedPatientId)
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest,
|
|
"When modifying a study, the parent PatientID cannot be manually modified");
|
|
}
|
|
|
|
if (level_ == ResourceType_Series && isReplacedPatientId)
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest,
|
|
"When modifying a series, the parent PatientID cannot be manually modified");
|
|
}
|
|
|
|
if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest,
|
|
"When modifying a series, the parent StudyInstanceUID cannot be manually modified");
|
|
}
|
|
|
|
if (level_ == ResourceType_Instance && isReplacedPatientId)
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest,
|
|
"When modifying an instance, the parent PatientID cannot be manually modified");
|
|
}
|
|
|
|
if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest,
|
|
"When modifying an instance, the parent StudyInstanceUID cannot be manually modified");
|
|
}
|
|
|
|
if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
|
|
{
|
|
throw OrthancException(ErrorCode_BadRequest,
|
|
"When modifying an instance, the parent SeriesInstanceUID cannot be manually modified");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void DicomModification::ParseAnonymizationRequest(bool& patientNameOverridden,
|
|
const Json::Value& request)
|
|
{
|
|
if (!request.isObject())
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat);
|
|
}
|
|
|
|
bool force = GetBooleanValue("Force", request, false);
|
|
|
|
// DicomVersion version = DicomVersion_2008; // For Orthanc <= 1.2.0
|
|
// DicomVersion version = DicomVersion_2017c; // For Orthanc between 1.3.0 and 1.9.3
|
|
// DicomVersion version = DicomVersion_2021b; // For Orthanc >= 1.9.4
|
|
DicomVersion version = DicomVersion_2023b; // For Orthanc >= 1.12.1
|
|
|
|
if (request.isMember("DicomVersion"))
|
|
{
|
|
if (request["DicomVersion"].type() != Json::stringValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat);
|
|
}
|
|
else
|
|
{
|
|
version = StringToDicomVersion(request["DicomVersion"].asString());
|
|
}
|
|
}
|
|
|
|
SetupAnonymization(version);
|
|
|
|
if (GetBooleanValue("KeepPrivateTags", request, false))
|
|
{
|
|
SetRemovePrivateTags(false);
|
|
}
|
|
|
|
if (GetBooleanValue("KeepLabels", request, false))
|
|
{
|
|
SetKeepLabels(true);
|
|
}
|
|
|
|
if (request.isMember("Remove"))
|
|
{
|
|
ParseListOfTags(*this, request["Remove"], TagOperation_Remove, force);
|
|
}
|
|
|
|
if (request.isMember("Replace"))
|
|
{
|
|
ParseReplacements(*this, request["Replace"], force);
|
|
}
|
|
|
|
if (request.isMember("Keep"))
|
|
{
|
|
ParseListOfTags(*this, request["Keep"], TagOperation_Keep, force);
|
|
}
|
|
|
|
patientNameOverridden = (uids_.find(DICOM_TAG_PATIENT_NAME) == uids_.end());
|
|
|
|
// New in Orthanc 1.6.0
|
|
if (request.isMember("PrivateCreator"))
|
|
{
|
|
privateCreator_ = SerializationToolbox::ReadString(request, "PrivateCreator");
|
|
}
|
|
}
|
|
|
|
void DicomModification::SetDicomIdentifierGenerator(DicomModification::IDicomIdentifierGenerator &generator)
|
|
{
|
|
identifierGenerator_ = &generator;
|
|
}
|
|
|
|
|
|
|
|
|
|
static const char* REMOVE_PRIVATE_TAGS = "RemovePrivateTags";
|
|
static const char* LEVEL = "Level";
|
|
static const char* ALLOW_MANUAL_IDENTIFIERS = "AllowManualIdentifiers";
|
|
static const char* KEEP_STUDY_INSTANCE_UID = "KeepStudyInstanceUID";
|
|
static const char* KEEP_SERIES_INSTANCE_UID = "KeepSeriesInstanceUID";
|
|
static const char* KEEP_SOP_INSTANCE_UID = "KeepSOPInstanceUID";
|
|
static const char* UPDATE_REFERENCED_RELATIONSHIPS = "UpdateReferencedRelationships";
|
|
static const char* IS_ANONYMIZATION = "IsAnonymization";
|
|
static const char* REMOVALS = "Removals";
|
|
static const char* CLEARINGS = "Clearings";
|
|
static const char* PRIVATE_TAGS_TO_KEEP = "PrivateTagsToKeep";
|
|
static const char* REPLACEMENTS = "Replacements";
|
|
static const char* MAP_PATIENTS = "MapPatients";
|
|
static const char* MAP_STUDIES = "MapStudies";
|
|
static const char* MAP_SERIES = "MapSeries";
|
|
static const char* MAP_INSTANCES = "MapInstances";
|
|
static const char* PRIVATE_CREATOR = "PrivateCreator"; // New in Orthanc 1.6.0
|
|
static const char* UIDS = "Uids"; // New in Orthanc 1.9.4
|
|
static const char* REMOVED_RANGES = "RemovedRanges"; // New in Orthanc 1.9.4
|
|
static const char* KEEP_SEQUENCES = "KeepSequences"; // New in Orthanc 1.9.4
|
|
static const char* REMOVE_SEQUENCES = "RemoveSequences"; // New in Orthanc 1.9.4
|
|
static const char* SEQUENCE_REPLACEMENTS = "SequenceReplacements"; // New in Orthanc 1.9.4
|
|
|
|
void DicomModification::Serialize(Json::Value& value) const
|
|
{
|
|
if (identifierGenerator_ != NULL)
|
|
{
|
|
throw OrthancException(ErrorCode_InternalError,
|
|
"Cannot serialize a DicomModification with a custom identifier generator");
|
|
}
|
|
|
|
value = Json::objectValue;
|
|
value[REMOVE_PRIVATE_TAGS] = removePrivateTags_;
|
|
value[LEVEL] = EnumerationToString(level_);
|
|
value[ALLOW_MANUAL_IDENTIFIERS] = allowManualIdentifiers_;
|
|
value[KEEP_STUDY_INSTANCE_UID] = keepStudyInstanceUid_;
|
|
value[KEEP_SERIES_INSTANCE_UID] = keepSeriesInstanceUid_;
|
|
value[KEEP_SOP_INSTANCE_UID] = keepSopInstanceUid_;
|
|
value[UPDATE_REFERENCED_RELATIONSHIPS] = updateReferencedRelationships_;
|
|
value[IS_ANONYMIZATION] = isAnonymization_;
|
|
value[PRIVATE_CREATOR] = privateCreator_;
|
|
|
|
SerializationToolbox::WriteSetOfTags(value, removals_, REMOVALS);
|
|
SerializationToolbox::WriteSetOfTags(value, clearings_, CLEARINGS);
|
|
SerializationToolbox::WriteSetOfTags(value, privateTagsToKeep_, PRIVATE_TAGS_TO_KEEP);
|
|
|
|
Json::Value& tmp = value[REPLACEMENTS];
|
|
|
|
tmp = Json::objectValue;
|
|
|
|
for (Replacements::const_iterator it = replacements_.begin();
|
|
it != replacements_.end(); ++it)
|
|
{
|
|
assert(it->second != NULL);
|
|
tmp[it->first.Format()] = *it->second;
|
|
}
|
|
|
|
Json::Value& mapPatients = value[MAP_PATIENTS];
|
|
Json::Value& mapStudies = value[MAP_STUDIES];
|
|
Json::Value& mapSeries = value[MAP_SERIES];
|
|
Json::Value& mapInstances = value[MAP_INSTANCES];
|
|
|
|
mapPatients = Json::objectValue;
|
|
mapStudies = Json::objectValue;
|
|
mapSeries = Json::objectValue;
|
|
mapInstances = Json::objectValue;
|
|
|
|
for (UidMap::const_iterator it = uidMap_.begin(); it != uidMap_.end(); ++it)
|
|
{
|
|
Json::Value* tmp2 = NULL;
|
|
|
|
switch (it->first.first)
|
|
{
|
|
case ResourceType_Patient:
|
|
tmp2 = &mapPatients;
|
|
break;
|
|
|
|
case ResourceType_Study:
|
|
tmp2 = &mapStudies;
|
|
break;
|
|
|
|
case ResourceType_Series:
|
|
tmp2 = &mapSeries;
|
|
break;
|
|
|
|
case ResourceType_Instance:
|
|
tmp2 = &mapInstances;
|
|
break;
|
|
|
|
default:
|
|
throw OrthancException(ErrorCode_InternalError);
|
|
}
|
|
|
|
assert(tmp2 != NULL);
|
|
(*tmp2) [it->first.second] = it->second;
|
|
}
|
|
|
|
// New in Orthanc 1.9.4
|
|
SerializationToolbox::WriteSetOfTags(value, uids_, UIDS);
|
|
|
|
// New in Orthanc 1.9.4
|
|
Json::Value ranges = Json::arrayValue;
|
|
|
|
for (RemovedRanges::const_iterator it = removedRanges_.begin(); it != removedRanges_.end(); ++it)
|
|
{
|
|
Json::Value item = Json::arrayValue;
|
|
item.append(it->GetGroupFrom());
|
|
item.append(it->GetGroupTo());
|
|
item.append(it->GetElementFrom());
|
|
item.append(it->GetElementTo());
|
|
ranges.append(item);
|
|
}
|
|
|
|
value[REMOVED_RANGES] = ranges;
|
|
|
|
// New in Orthanc 1.9.4
|
|
Json::Value lst = Json::arrayValue;
|
|
for (ListOfPaths::const_iterator it = keepSequences_.begin(); it != keepSequences_.end(); ++it)
|
|
{
|
|
lst.append(it->Format());
|
|
}
|
|
|
|
value[KEEP_SEQUENCES] = lst;
|
|
|
|
// New in Orthanc 1.9.4
|
|
lst = Json::arrayValue;
|
|
for (ListOfPaths::const_iterator it = removeSequences_.begin(); it != removeSequences_.end(); ++it)
|
|
{
|
|
assert(it->GetPrefixLength() > 0);
|
|
lst.append(it->Format());
|
|
}
|
|
|
|
value[REMOVE_SEQUENCES] = lst;
|
|
|
|
// New in Orthanc 1.9.4
|
|
lst = Json::objectValue;
|
|
for (SequenceReplacements::const_iterator it = sequenceReplacements_.begin(); it != sequenceReplacements_.end(); ++it)
|
|
{
|
|
assert(*it != NULL);
|
|
assert((*it)->GetPath().GetPrefixLength() > 0);
|
|
lst[(*it)->GetPath().Format()] = (*it)->GetValue();
|
|
}
|
|
|
|
value[SEQUENCE_REPLACEMENTS] = lst;
|
|
}
|
|
|
|
void DicomModification::UnserializeUidMap(ResourceType level,
|
|
const Json::Value& serialized,
|
|
const char* field)
|
|
{
|
|
if (!serialized.isMember(field) ||
|
|
serialized[field].type() != Json::objectValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat);
|
|
}
|
|
|
|
Json::Value::Members names = serialized[field].getMemberNames();
|
|
|
|
for (Json::Value::Members::const_iterator it = names.begin(); it != names.end(); ++it)
|
|
{
|
|
const Json::Value& value = serialized[field][*it];
|
|
|
|
if (value.type() != Json::stringValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat);
|
|
}
|
|
else
|
|
{
|
|
uidMap_[std::make_pair(level, *it)] = value.asString();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
DicomModification::DicomModification(const Json::Value& serialized) :
|
|
identifierGenerator_(NULL)
|
|
{
|
|
removePrivateTags_ = SerializationToolbox::ReadBoolean(serialized, REMOVE_PRIVATE_TAGS);
|
|
level_ = StringToResourceType(SerializationToolbox::ReadString(serialized, LEVEL).c_str());
|
|
allowManualIdentifiers_ = SerializationToolbox::ReadBoolean(serialized, ALLOW_MANUAL_IDENTIFIERS);
|
|
keepStudyInstanceUid_ = SerializationToolbox::ReadBoolean(serialized, KEEP_STUDY_INSTANCE_UID);
|
|
keepSeriesInstanceUid_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SERIES_INSTANCE_UID);
|
|
updateReferencedRelationships_ = SerializationToolbox::ReadBoolean
|
|
(serialized, UPDATE_REFERENCED_RELATIONSHIPS);
|
|
isAnonymization_ = SerializationToolbox::ReadBoolean(serialized, IS_ANONYMIZATION);
|
|
|
|
if (serialized.isMember(KEEP_SOP_INSTANCE_UID))
|
|
{
|
|
keepSopInstanceUid_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOP_INSTANCE_UID);
|
|
}
|
|
else
|
|
{
|
|
/**
|
|
* Compatibility with jobs serialized using Orthanc between
|
|
* 1.5.0 and 1.6.1. This compatibility was broken between 1.7.0
|
|
* and 1.9.3: Indeed, an exception was thrown in "ReadBoolean()"
|
|
* if "KEEP_SOP_INSTANCE_UID" was absent, because of changeset:
|
|
* https://orthanc.uclouvain.be/hg/orthanc/rev/3860
|
|
**/
|
|
keepSopInstanceUid_ = false;
|
|
}
|
|
|
|
if (serialized.isMember(PRIVATE_CREATOR))
|
|
{
|
|
privateCreator_ = SerializationToolbox::ReadString(serialized, PRIVATE_CREATOR);
|
|
}
|
|
|
|
SerializationToolbox::ReadSetOfTags(removals_, serialized, REMOVALS);
|
|
SerializationToolbox::ReadSetOfTags(clearings_, serialized, CLEARINGS);
|
|
SerializationToolbox::ReadSetOfTags(privateTagsToKeep_, serialized, PRIVATE_TAGS_TO_KEEP);
|
|
|
|
if (!serialized.isMember(REPLACEMENTS) ||
|
|
serialized[REPLACEMENTS].type() != Json::objectValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat);
|
|
}
|
|
|
|
Json::Value::Members names = serialized[REPLACEMENTS].getMemberNames();
|
|
|
|
for (Json::Value::Members::const_iterator it = names.begin(); it != names.end(); ++it)
|
|
{
|
|
DicomTag tag(0, 0);
|
|
if (!DicomTag::ParseHexadecimal(tag, it->c_str()))
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat);
|
|
}
|
|
else
|
|
{
|
|
const Json::Value& value = serialized[REPLACEMENTS][*it];
|
|
replacements_.insert(std::make_pair(tag, new Json::Value(value)));
|
|
}
|
|
}
|
|
|
|
UnserializeUidMap(ResourceType_Patient, serialized, MAP_PATIENTS);
|
|
UnserializeUidMap(ResourceType_Study, serialized, MAP_STUDIES);
|
|
UnserializeUidMap(ResourceType_Series, serialized, MAP_SERIES);
|
|
UnserializeUidMap(ResourceType_Instance, serialized, MAP_INSTANCES);
|
|
|
|
// New in Orthanc 1.9.4
|
|
if (serialized.isMember(UIDS)) // Backward compatibility with Orthanc <= 1.9.3
|
|
{
|
|
SerializationToolbox::ReadSetOfTags(uids_, serialized, UIDS);
|
|
}
|
|
else
|
|
{
|
|
SetupUidsFromOrthanc_1_9_3();
|
|
}
|
|
|
|
// New in Orthanc 1.9.4
|
|
removedRanges_.clear();
|
|
if (serialized.isMember(REMOVED_RANGES)) // Backward compatibility with Orthanc <= 1.9.3
|
|
{
|
|
const Json::Value& ranges = serialized[REMOVED_RANGES];
|
|
|
|
if (ranges.type() != Json::arrayValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat);
|
|
}
|
|
else
|
|
{
|
|
for (Json::Value::ArrayIndex i = 0; i < ranges.size(); i++)
|
|
{
|
|
if (ranges[i].type() != Json::arrayValue ||
|
|
ranges[i].size() != 4 ||
|
|
!ranges[i][0].isUInt() ||
|
|
!ranges[i][1].isUInt() ||
|
|
!ranges[i][2].isUInt() ||
|
|
!ranges[i][3].isUInt())
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat);
|
|
}
|
|
else
|
|
{
|
|
Json::LargestUInt groupFrom = ranges[i][0].asUInt();
|
|
Json::LargestUInt groupTo = ranges[i][1].asUInt();
|
|
Json::LargestUInt elementFrom = ranges[i][2].asUInt();
|
|
Json::LargestUInt elementTo = ranges[i][3].asUInt();
|
|
|
|
if (groupFrom > groupTo ||
|
|
elementFrom > elementTo ||
|
|
groupTo > 0xffffu ||
|
|
elementTo > 0xffffu)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat);
|
|
}
|
|
else
|
|
{
|
|
removedRanges_.push_back(DicomTagRange(groupFrom, groupTo, elementFrom, elementTo));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// New in Orthanc 1.9.4
|
|
if (serialized.isMember(KEEP_SEQUENCES))
|
|
{
|
|
const Json::Value& keep = serialized[KEEP_SEQUENCES];
|
|
|
|
if (keep.type() != Json::arrayValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat);
|
|
}
|
|
else
|
|
{
|
|
for (Json::Value::ArrayIndex i = 0; i < keep.size(); i++)
|
|
{
|
|
if (keep[i].type() != Json::stringValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat);
|
|
}
|
|
else
|
|
{
|
|
keepSequences_.push_back(DicomPath::Parse(keep[i].asString()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// New in Orthanc 1.9.4
|
|
if (serialized.isMember(REMOVE_SEQUENCES))
|
|
{
|
|
const Json::Value& remove = serialized[REMOVE_SEQUENCES];
|
|
|
|
if (remove.type() != Json::arrayValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat);
|
|
}
|
|
else
|
|
{
|
|
for (Json::Value::ArrayIndex i = 0; i < remove.size(); i++)
|
|
{
|
|
if (remove[i].type() != Json::stringValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat);
|
|
}
|
|
else
|
|
{
|
|
removeSequences_.push_back(DicomPath::Parse(remove[i].asString()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// New in Orthanc 1.9.4
|
|
if (serialized.isMember(SEQUENCE_REPLACEMENTS))
|
|
{
|
|
const Json::Value& replace = serialized[SEQUENCE_REPLACEMENTS];
|
|
|
|
if (replace.type() != Json::objectValue)
|
|
{
|
|
throw OrthancException(ErrorCode_BadFileFormat);
|
|
}
|
|
else
|
|
{
|
|
Json::Value::Members members = replace.getMemberNames();
|
|
for (size_t i = 0; i < members.size(); i++)
|
|
{
|
|
sequenceReplacements_.push_back(
|
|
new SequenceReplacement(DicomPath::Parse(members[i]), replace[members[i]]));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void DicomModification::SetPrivateCreator(const std::string &privateCreator)
|
|
{
|
|
privateCreator_ = privateCreator;
|
|
}
|
|
|
|
const std::string &DicomModification::GetPrivateCreator() const
|
|
{
|
|
return privateCreator_;
|
|
}
|
|
|
|
|
|
void DicomModification::Keep(const DicomPath& path)
|
|
{
|
|
if (path.GetPrefixLength() == 0)
|
|
{
|
|
Keep(path.GetFinalTag());
|
|
}
|
|
|
|
keepSequences_.push_back(path);
|
|
MarkNotOrthancAnonymization();
|
|
}
|
|
|
|
|
|
void DicomModification::Remove(const DicomPath& path)
|
|
{
|
|
if (path.GetPrefixLength() == 0)
|
|
{
|
|
Remove(path.GetFinalTag());
|
|
}
|
|
else
|
|
{
|
|
removeSequences_.push_back(path);
|
|
MarkNotOrthancAnonymization();
|
|
}
|
|
}
|
|
|
|
|
|
void DicomModification::Replace(const DicomPath& path,
|
|
const Json::Value& value,
|
|
bool safeForAnonymization)
|
|
{
|
|
if (path.GetPrefixLength() == 0)
|
|
{
|
|
Replace(path.GetFinalTag(), value, safeForAnonymization);
|
|
}
|
|
else
|
|
{
|
|
sequenceReplacements_.push_back(new SequenceReplacement(path, value));
|
|
|
|
if (!safeForAnonymization)
|
|
{
|
|
MarkNotOrthancAnonymization();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
bool DicomModification::IsAlteredTag(const DicomTag& tag) const
|
|
{
|
|
return (uids_.find(tag) != uids_.end() ||
|
|
IsCleared(tag) ||
|
|
IsRemoved(tag) ||
|
|
IsReplaced(tag) ||
|
|
(tag.IsPrivate() &&
|
|
ArePrivateTagsRemoved() &&
|
|
privateTagsToKeep_.find(tag) == privateTagsToKeep_.end()) ||
|
|
(isAnonymization_ && (
|
|
tag == DICOM_TAG_PATIENT_NAME ||
|
|
tag == DICOM_TAG_PATIENT_ID)) ||
|
|
(tag == DICOM_TAG_STUDY_INSTANCE_UID &&
|
|
!keepStudyInstanceUid_) ||
|
|
(tag == DICOM_TAG_SERIES_INSTANCE_UID &&
|
|
!keepSeriesInstanceUid_) ||
|
|
(tag == DICOM_TAG_SOP_INSTANCE_UID &&
|
|
!keepSopInstanceUid_));
|
|
}
|
|
|
|
void DicomModification::GetReplacedTags(std::set<DicomTag>& target) const
|
|
{
|
|
target.clear();
|
|
for (Replacements::const_iterator it = replacements_.begin(); it != replacements_.end(); ++it)
|
|
{
|
|
target.insert(it->first);
|
|
}
|
|
}
|
|
}
|