Mercurial > hg > orthanc
view OrthancFramework/Sources/DicomParsing/DicomModification.cpp @ 5818:86cd21ef81fb
news
author | Alain Mazy <am@orthanc.team> |
---|---|
date | Fri, 27 Sep 2024 07:51:38 +0200 |
parents | f7adfb22e20e |
children |
line wrap: on
line source
/** * 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-2024 Orthanc Team SRL, Belgium * Copyright (C) 2021-2024 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"; 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) { delete it->second; replacements_.erase(it); } } } 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(DicomTag(0x0012, 0x0062), "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); } } }