PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
anonymizer.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
14
15#include <algorithm>
16#include <array>
17#include <chrono>
18#include <functional>
19#include <iomanip>
20#include <memory>
21#include <random>
22#include <sstream>
23#include <vector>
24
25#ifdef PACS_WITH_DIGITAL_SIGNATURES
26 #include <openssl/evp.h>
27 #include <openssl/rand.h>
28#endif
29
30namespace kcenon::pacs::security {
31
32using namespace kcenon::pacs::core;
33
38
40 : profile_{other.profile_}
41 , custom_actions_{other.custom_actions_}
42 , date_offset_{other.date_offset_}
43 , encryption_key_{other.encryption_key_}
44 , hash_salt_{other.hash_salt_}
45 , private_tag_action_{other.private_tag_action_}
46 , detailed_reporting_{other.detailed_reporting_} {}
47
49 : profile_{other.profile_}
50 , custom_actions_{std::move(other.custom_actions_)}
51 , date_offset_{other.date_offset_}
52 , encryption_key_{std::move(other.encryption_key_)}
53 , hash_salt_{std::move(other.hash_salt_)}
54 , private_tag_action_{other.private_tag_action_}
55 , detailed_reporting_{other.detailed_reporting_} {}
56
58 if (this != &other) {
59 profile_ = other.profile_;
60 custom_actions_ = other.custom_actions_;
61 date_offset_ = other.date_offset_;
62 encryption_key_ = other.encryption_key_;
63 hash_salt_ = other.hash_salt_;
64 private_tag_action_ = other.private_tag_action_;
65 detailed_reporting_ = other.detailed_reporting_;
66 }
67 return *this;
68}
69
71 if (this != &other) {
72 profile_ = other.profile_;
73 custom_actions_ = std::move(other.custom_actions_);
74 date_offset_ = other.date_offset_;
75 encryption_key_ = std::move(other.encryption_key_);
76 hash_salt_ = std::move(other.hash_salt_);
77 private_tag_action_ = other.private_tag_action_;
78 detailed_reporting_ = other.detailed_reporting_;
79 }
80 return *this;
81}
82
85 uid_mapping temp_mapping;
86 return anonymize_with_mapping(dataset, temp_mapping);
87}
88
90 dicom_dataset& dataset,
91 uid_mapping& mapping
94 report.profile_name = std::string(to_string(profile_));
95 report.date_offset = date_offset_;
96 report.timestamp = std::chrono::system_clock::now();
97
98 // Get profile actions
99 auto profile_actions = get_profile_actions(profile_);
100
101 // Merge with custom actions (custom takes precedence)
102 for (const auto& [tag, config] : custom_actions_) {
103 profile_actions[tag] = config;
104 }
105
106 // Process each tag in the profile
107 for (const auto& [tag, config] : profile_actions) {
108 if (!dataset.contains(tag)) {
109 continue;
110 }
111
112 auto record = apply_action(dataset, tag, config, &mapping);
113 report.total_tags_processed++;
114
115 switch (config.action) {
118 if (record.success) {
119 report.tags_removed++;
120 }
121 break;
123 if (record.success) {
124 report.tags_emptied++;
125 }
126 break;
128 if (record.success) {
129 report.tags_replaced++;
130 }
131 break;
133 if (record.success) {
134 report.uids_replaced++;
135 }
136 break;
137 case tag_action::keep:
138 report.tags_kept++;
139 break;
141 if (record.success) {
142 report.dates_shifted++;
143 }
144 break;
145 case tag_action::hash:
146 if (record.success) {
147 report.values_hashed++;
148 }
149 break;
151 if (record.success) {
152 report.tags_replaced++;
153 }
154 break;
155 }
156
157 if (!record.success && !record.error_message.empty()) {
158 report.errors.push_back(record.error_message);
159 }
160
161 if (detailed_reporting_) {
162 report.action_records.push_back(std::move(record));
163 }
164 }
165
166 // Remove private tags if configured
167 if (private_tag_action_ != private_tag_action::keep) {
168 std::vector<dicom_tag> tags_to_remove;
169
170 for (auto it = dataset.begin(); it != dataset.end(); ++it) {
171 auto tag = it->first;
172 if (private_tag_action_ == private_tag_action::remove_all) {
173 if (tag.is_private()) {
174 tags_to_remove.push_back(tag);
175 }
176 } else if (private_tag_action_ == private_tag_action::remove_data) {
177 if (tag.is_private_data()) {
178 tags_to_remove.push_back(tag);
179 }
180 }
181 }
182
183 for (const auto& tag : tags_to_remove) {
184 if (dataset.remove(tag)) {
185 report.private_tags_removed++;
186 }
187 }
188 }
189
190 return report;
191}
192
194 return profile_;
195}
196
201
205
208 return private_tag_action_;
209}
210
212 custom_actions_[tag] = std::move(config);
213}
214
216 const std::map<dicom_tag, tag_action_config>& actions
217) {
218 for (const auto& [tag, config] : actions) {
219 custom_actions_[tag] = config;
220 }
221}
222
224 return custom_actions_.erase(tag) > 0;
225}
226
230
232 auto it = custom_actions_.find(tag);
233 if (it != custom_actions_.end()) {
234 return it->second;
235 }
236
237 auto profile_actions = get_profile_actions(profile_);
238 auto profile_it = profile_actions.find(tag);
239 if (profile_it != profile_actions.end()) {
240 return profile_it->second;
241 }
242
244}
245
246void anonymizer::set_date_offset(std::chrono::days offset) {
247 date_offset_ = offset;
248}
249
250auto anonymizer::get_date_offset() const noexcept
251 -> std::optional<std::chrono::days> {
252 return date_offset_;
253}
254
258
260 std::chrono::days min_days,
261 std::chrono::days max_days
262) -> std::chrono::days {
263 std::random_device rd;
264 std::mt19937 gen(rd());
265 std::uniform_int_distribution<int> dist(
266 static_cast<int>(min_days.count()),
267 static_cast<int>(max_days.count())
268 );
269 return std::chrono::days{dist(gen)};
270}
271
272auto anonymizer::set_encryption_key(std::span<const std::uint8_t> key)
273 -> kcenon::common::VoidResult {
274 if (key.size() != 32) {
275 return kcenon::common::make_error<std::monostate>(
276 1, "Encryption key must be 32 bytes for AES-256", "anonymizer"
277 );
278 }
279 encryption_key_.assign(key.begin(), key.end());
280 return kcenon::common::ok();
281}
282
283auto anonymizer::has_encryption_key() const noexcept -> bool {
284 return !encryption_key_.empty();
285}
286
287void anonymizer::set_hash_salt(std::string salt) {
288 hash_salt_ = std::move(salt);
289}
290
291auto anonymizer::get_hash_salt() const -> std::optional<std::string> {
292 return hash_salt_;
293}
294
296 detailed_reporting_ = enable;
297}
298
299auto anonymizer::is_detailed_reporting() const noexcept -> bool {
300 return detailed_reporting_;
301}
302
304 -> std::map<dicom_tag, tag_action_config> {
305 std::map<dicom_tag, tag_action_config> actions;
306
307 // Common patient identifiers
308 auto add_patient_identifiers = [&actions]() {
316 };
317
318 // Institution identifiers
319 auto add_institution_identifiers = [&actions]() {
323 };
324
325 // Personnel identifiers
326 auto add_personnel_identifiers = [&actions]() {
332 };
333
334 // UID replacements
335 auto add_uid_replacements = [&actions]() {
336 actions[tags::study_instance_uid] =
337 {.action = tag_action::replace_uid};
339 {.action = tag_action::replace_uid};
340 actions[tags::sop_instance_uid] =
341 {.action = tag_action::replace_uid};
343 {.action = tag_action::replace_uid};
344 };
345
346 // Study identifiers
347 auto add_study_identifiers = [&actions]() {
349 actions[tags::study_id] = tag_action_config::make_replace("ANON_STUDY");
350 };
351
352 // Dates for shifting
353 auto add_date_shifting = [&actions]() {
354 actions[tags::study_date] = {.action = tag_action::shift_date};
355 actions[tags::series_date] = {.action = tag_action::shift_date};
356 actions[tags::acquisition_date] = {.action = tag_action::shift_date};
357 actions[tags::content_date] = {.action = tag_action::shift_date};
360 };
361
362 switch (profile) {
364 add_patient_identifiers();
365 add_institution_identifiers();
366 add_personnel_identifiers();
367 add_uid_replacements();
368 add_study_identifiers();
369 break;
370
372 add_patient_identifiers();
373 add_institution_identifiers();
374 add_personnel_identifiers();
375 add_uid_replacements();
376 add_study_identifiers();
377 // Note: Pixel data cleaning requires image processing
378 // which is not implemented in this basic version
379 break;
380
382 add_patient_identifiers();
383 add_institution_identifiers();
384 add_personnel_identifiers();
385 add_uid_replacements();
386 add_study_identifiers();
389 break;
390
392 add_patient_identifiers();
393 add_institution_identifiers();
394 add_personnel_identifiers();
395 add_uid_replacements();
396 add_study_identifiers();
397 add_date_shifting();
398 break;
399
401 add_patient_identifiers();
402 add_institution_identifiers();
403 add_personnel_identifiers();
404 add_uid_replacements();
405 add_study_identifiers();
406 // Keep demographic data
411 break;
412
414 add_patient_identifiers();
415 add_institution_identifiers();
416 add_personnel_identifiers();
417 add_uid_replacements();
418 add_study_identifiers();
419 // Remove all HIPAA identifiers
420 for (const auto& tag : get_hipaa_identifier_tags()) {
421 if (actions.find(tag) == actions.end()) {
422 actions[tag] = tag_action_config::make_remove();
423 }
424 }
425 // Dates - keep only year or remove
427 break;
428
430 add_patient_identifiers();
431 add_institution_identifiers();
432 add_personnel_identifiers();
433 add_uid_replacements();
434 add_study_identifiers();
435 // GDPR allows pseudonymization - use hash for linkage
438 break;
439 }
440
441 return actions;
442}
443
444auto anonymizer::get_hipaa_identifier_tags() -> std::vector<dicom_tag> {
446}
447
448auto anonymizer::get_gdpr_personal_data_tags() -> std::vector<dicom_tag> {
449 std::vector<dicom_tag> tags;
450
451 // Direct identifiers
452 tags.push_back(tags::patient_name);
453 tags.push_back(tags::patient_id);
454 tags.push_back(tags::patient_birth_date);
455 tags.push_back(tags::patient_address);
456
457 // Indirect identifiers
458 tags.push_back(tags::study_instance_uid);
459 tags.push_back(tags::series_instance_uid);
460 tags.push_back(tags::sop_instance_uid);
461
462 // Health data (special category under GDPR)
463 tags.push_back(tags::patient_sex);
464 tags.push_back(tags::patient_age);
465 tags.push_back(tags::patient_size);
466 tags.push_back(tags::patient_weight);
467
468 return tags;
469}
470
472 dicom_dataset& dataset,
473 dicom_tag tag,
474 const tag_action_config& config,
475 uid_mapping* mapping
477 tag_action_record record;
478 record.tag = tag;
479 record.action = config.action;
480
481 auto* element = dataset.get(tag);
482 if (element == nullptr) {
483 record.success = false;
484 record.error_message = "Tag not found in dataset";
485 return record;
486 }
487
488 record.original_value = element->as_string().unwrap_or("");
489
490 switch (config.action) {
493 record.success = dataset.remove(tag);
494 break;
495
497 dataset.set_string(tag, element->vr(), "");
498 record.new_value = "";
499 record.success = true;
500 break;
501
502 case tag_action::keep:
503 record.new_value = record.original_value;
504 record.success = true;
505 break;
506
508 dataset.set_string(tag, element->vr(), config.replacement_value);
509 record.new_value = config.replacement_value;
510 record.success = true;
511 break;
512
514 if (mapping != nullptr) {
515 auto result = mapping->get_or_create(record.original_value);
516 if (result.is_ok()) {
517 dataset.set_string(tag, element->vr(), result.value());
518 record.new_value = result.value();
519 record.success = true;
520 } else {
521 record.success = false;
522 record.error_message = "Failed to create UID mapping";
523 }
524 } else {
525 // Generate new UID without mapping
526 uid_mapping temp;
527 auto result = temp.get_or_create(record.original_value);
528 if (result.is_ok()) {
529 dataset.set_string(tag, element->vr(), result.value());
530 record.new_value = result.value();
531 record.success = true;
532 } else {
533 record.success = false;
534 record.error_message = "Failed to generate new UID";
535 }
536 }
537 break;
538
539 case tag_action::hash: {
540 auto hashed = hash_value(record.original_value);
541 if (hashed.empty()) {
542 record.success = false;
543 record.error_message = "Hash computation failed";
544 } else {
545 dataset.set_string(tag, element->vr(), hashed);
546 record.new_value = hashed;
547 record.success = true;
548 }
549 break;
550 }
551
552 case tag_action::encrypt: {
553 auto result = encrypt_value(record.original_value);
554 if (result.is_ok()) {
555 dataset.set_string(tag, element->vr(), result.value());
556 record.new_value = result.value();
557 record.success = true;
558 } else {
559 record.success = false;
560 record.error_message = result.error().message;
561 }
562 break;
563 }
564
566 if (date_offset_.has_value()) {
567 auto shifted = shift_date(record.original_value);
568 dataset.set_string(tag, element->vr(), shifted);
569 record.new_value = shifted;
570 record.success = true;
571 } else {
572 // No offset set - empty the date instead
573 dataset.set_string(tag, element->vr(), "");
574 record.new_value = "";
575 record.success = true;
576 }
577 break;
578 }
579 }
580
581 return record;
582}
583
584auto anonymizer::shift_date(std::string_view date_string) const -> std::string {
585 if (date_string.empty() || !date_offset_.has_value()) {
586 return "";
587 }
588
589 // Parse DICOM date format (YYYYMMDD)
590 if (date_string.length() < 8) {
591 return std::string(date_string);
592 }
593
594 try {
595 int year = std::stoi(std::string(date_string.substr(0, 4)));
596 int month = std::stoi(std::string(date_string.substr(4, 2)));
597 int day = std::stoi(std::string(date_string.substr(6, 2)));
598
599 // Create time point from date
600 std::tm tm = {};
601 tm.tm_year = year - 1900;
602 tm.tm_mon = month - 1;
603 tm.tm_mday = day;
604
605 auto time_point = std::chrono::system_clock::from_time_t(std::mktime(&tm));
606
607 // Apply offset
608 time_point += date_offset_.value();
609
610 // Convert back to date string
611 auto shifted_time = std::chrono::system_clock::to_time_t(time_point);
612 std::tm shifted_tm{};
613#if defined(_WIN32)
614 localtime_s(&shifted_tm, &shifted_time);
615#else
616 localtime_r(&shifted_time, &shifted_tm);
617#endif
618
619 std::ostringstream oss;
620 oss << std::setfill('0')
621 << std::setw(4) << (shifted_tm.tm_year + 1900)
622 << std::setw(2) << (shifted_tm.tm_mon + 1)
623 << std::setw(2) << shifted_tm.tm_mday;
624
625 return oss.str();
626
627 } catch (const std::exception&) {
628 return std::string(date_string);
629 }
630}
631
632auto anonymizer::hash_value(std::string_view value) const -> std::string {
633 std::string to_hash = std::string(value);
634
635 if (hash_salt_.has_value()) {
636 to_hash = hash_salt_.value() + to_hash;
637 }
638
639#ifdef PACS_WITH_DIGITAL_SIGNATURES
640 auto* ctx = EVP_MD_CTX_new();
641 if (ctx == nullptr) {
642 return {};
643 }
644
645 struct md_ctx_deleter {
646 void operator()(EVP_MD_CTX* c) const { EVP_MD_CTX_free(c); }
647 };
648 std::unique_ptr<EVP_MD_CTX, md_ctx_deleter> ctx_guard(ctx);
649
650 if (EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr) != 1
651 || EVP_DigestUpdate(ctx, to_hash.data(), to_hash.size()) != 1) {
652 return {};
653 }
654
655 std::array<unsigned char, EVP_MAX_MD_SIZE> digest{};
656 unsigned int digest_len = 0;
657 if (EVP_DigestFinal_ex(ctx, digest.data(), &digest_len) != 1) {
658 return {};
659 }
660
661 std::ostringstream oss;
662 oss << std::hex << std::setfill('0');
663 for (unsigned int i = 0; i < digest_len; ++i) {
664 oss << std::setw(2) << static_cast<int>(digest[i]);
665 }
666 return oss.str();
667#else
668 // Non-cryptographic fallback when OpenSSL is unavailable
669 auto hash = std::hash<std::string>{}(to_hash);
670 std::ostringstream oss;
671 oss << std::hex << std::setfill('0') << std::setw(16) << hash;
672 return oss.str();
673#endif
674}
675
676auto anonymizer::encrypt_value(std::string_view value) const
678 if (encryption_key_.empty()) {
679 return kcenon::common::make_error<std::string>(
680 1, "No encryption key configured");
681 }
682
683#ifdef PACS_WITH_DIGITAL_SIGNATURES
684 constexpr int iv_length = 12;
685 constexpr int tag_length = 16;
686
687 // Generate random IV
688 std::array<unsigned char, iv_length> iv{};
689 if (RAND_bytes(iv.data(), iv_length) != 1) {
690 return kcenon::common::make_error<std::string>(
691 2, "Failed to generate random IV");
692 }
693
694 // Create cipher context with RAII
695 struct cipher_ctx_deleter {
696 void operator()(EVP_CIPHER_CTX* c) const { EVP_CIPHER_CTX_free(c); }
697 };
698
699 std::unique_ptr<EVP_CIPHER_CTX, cipher_ctx_deleter> ctx(
700 EVP_CIPHER_CTX_new());
701 if (!ctx) {
702 return kcenon::common::make_error<std::string>(
703 3, "Failed to create cipher context");
704 }
705
706 // Initialize AES-256-GCM
707 if (EVP_EncryptInit_ex(
708 ctx.get(), EVP_aes_256_gcm(), nullptr, nullptr, nullptr)
709 != 1) {
710 return kcenon::common::make_error<std::string>(
711 4, "Failed to initialize AES-256-GCM");
712 }
713
714 if (EVP_CIPHER_CTX_ctrl(
715 ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv_length, nullptr)
716 != 1) {
717 return kcenon::common::make_error<std::string>(
718 5, "Failed to set IV length");
719 }
720
721 if (EVP_EncryptInit_ex(ctx.get(), nullptr, nullptr,
722 encryption_key_.data(), iv.data())
723 != 1) {
724 return kcenon::common::make_error<std::string>(
725 6, "Failed to set encryption key and IV");
726 }
727
728 // Encrypt
729 std::vector<unsigned char> ciphertext(value.size() + EVP_MAX_BLOCK_LENGTH);
730 int out_len = 0;
731 if (EVP_EncryptUpdate(
732 ctx.get(), ciphertext.data(), &out_len,
733 reinterpret_cast<const unsigned char*>(value.data()),
734 static_cast<int>(value.size()))
735 != 1) {
736 return kcenon::common::make_error<std::string>(
737 7, "Encryption failed");
738 }
739 int ciphertext_len = out_len;
740
741 // Finalize
742 if (EVP_EncryptFinal_ex(
743 ctx.get(), ciphertext.data() + out_len, &out_len)
744 != 1) {
745 return kcenon::common::make_error<std::string>(
746 8, "Encryption finalization failed");
747 }
748 ciphertext_len += out_len;
749
750 // Get authentication tag
751 std::array<unsigned char, tag_length> tag{};
752 if (EVP_CIPHER_CTX_ctrl(
753 ctx.get(), EVP_CTRL_GCM_GET_TAG, tag_length, tag.data())
754 != 1) {
755 return kcenon::common::make_error<std::string>(
756 9, "Failed to get authentication tag");
757 }
758
759 // Encode as hex: IV || ciphertext || tag
760 std::ostringstream oss;
761 oss << std::hex << std::setfill('0');
762 for (int i = 0; i < iv_length; ++i) {
763 oss << std::setw(2) << static_cast<int>(iv[i]);
764 }
765 for (int i = 0; i < ciphertext_len; ++i) {
766 oss << std::setw(2) << static_cast<int>(ciphertext[i]);
767 }
768 for (int i = 0; i < tag_length; ++i) {
769 oss << std::setw(2) << static_cast<int>(tag[i]);
770 }
771
772 return kcenon::common::Result<std::string>(oss.str());
773#else
774 (void)value;
775 return kcenon::common::make_error<std::string>(
776 1, "Encryption requires OpenSSL (build with PACS_WITH_DIGITAL_SIGNATURES)");
777#endif
778}
779
781 // Profile actions are computed on-demand in get_profile_actions()
782 // This method can be used for any initialization if needed
783}
784
785} // namespace kcenon::pacs::security
void clear_custom_actions()
Clear all custom tag actions.
auto hash_value(std::string_view value) const -> std::string
static auto get_hipaa_identifier_tags() -> std::vector< core::dicom_tag >
Get a list of HIPAA Safe Harbor identifier tags.
private_tag_action private_tag_action_
Action for private tags.
Definition anonymizer.h:389
void set_detailed_reporting(bool enable)
Enable detailed action recording.
std::optional< std::string > hash_salt_
Hash salt.
Definition anonymizer.h:386
auto apply_action(core::dicom_dataset &dataset, core::dicom_tag tag, const tag_action_config &config, uid_mapping *mapping) -> tag_action_record
void add_tag_action(core::dicom_tag tag, tag_action_config config)
Add or override a tag action.
void set_hash_salt(std::string salt)
Set salt for hash operations.
anonymizer(anonymization_profile profile=anonymization_profile::basic)
Construct with a specific profile.
static auto get_gdpr_personal_data_tags() -> std::vector< core::dicom_tag >
Get a list of GDPR personal data tags.
auto get_tag_action(core::dicom_tag tag) const -> tag_action_config
Get the effective action for a tag.
std::optional< std::chrono::days > date_offset_
Date offset for shifting.
Definition anonymizer.h:380
void set_profile(anonymization_profile profile)
Set a new profile.
auto get_date_offset() const noexcept -> std::optional< std::chrono::days >
Get the current date offset.
static auto get_profile_actions(anonymization_profile profile) -> std::map< core::dicom_tag, tag_action_config >
Get tags to process for a given profile.
std::vector< std::uint8_t > encryption_key_
Encryption key (if set)
Definition anonymizer.h:383
void clear_date_offset()
Clear the date offset (dates will be zeroed instead)
void add_tag_actions(const std::map< core::dicom_tag, tag_action_config > &actions)
Add multiple tag actions.
auto anonymize_with_mapping(core::dicom_dataset &dataset, uid_mapping &mapping) -> kcenon::common::Result< anonymization_report >
Anonymize with consistent UID mapping.
auto shift_date(std::string_view date_string) const -> std::string
auto has_encryption_key() const noexcept -> bool
Check if encryption is configured.
auto get_hash_salt() const -> std::optional< std::string >
Get the current hash salt.
static auto generate_random_date_offset(std::chrono::days min_days=std::chrono::days{-365}, std::chrono::days max_days=std::chrono::days{365}) -> std::chrono::days
Generate a random date offset.
anonymization_profile profile_
Current anonymization profile.
Definition anonymizer.h:374
auto set_encryption_key(std::span< const std::uint8_t > key) -> kcenon::common::VoidResult
Set encryption key for encrypt actions.
void set_date_offset(std::chrono::days offset)
Set date offset for longitudinal consistency.
auto encrypt_value(std::string_view value) const -> kcenon::common::Result< std::string >
auto get_profile() const noexcept -> anonymization_profile
Get the current profile.
auto is_detailed_reporting() const noexcept -> bool
Check if detailed reporting is enabled.
bool detailed_reporting_
Whether to include detailed action records in report.
Definition anonymizer.h:392
auto get_private_tag_action() const noexcept -> private_tag_action
Get the current private tag action.
auto remove_tag_action(core::dicom_tag tag) -> bool
Remove a custom tag action (reverts to profile default)
std::map< core::dicom_tag, tag_action_config > custom_actions_
Custom tag actions (override profile defaults)
Definition anonymizer.h:377
auto anonymize(core::dicom_dataset &dataset) -> kcenon::common::Result< anonymization_report >
Anonymize a DICOM dataset.
auto operator=(const anonymizer &other) -> anonymizer &
Copy assignment.
void set_private_tag_action(private_tag_action action)
Set the action to take on private tags during anonymization.
auto get_or_create(std::string_view original_uid) -> kcenon::common::Result< std::string >
Get existing mapping or create new one.
Compile-time constants for commonly used DICOM tags.
DICOM de-identification/anonymization per PS3.15 Annex E.
constexpr dicom_tag patient_comments
Patient Comments.
constexpr dicom_tag referring_physician_name
Referring Physician's Name.
constexpr dicom_tag institution_name
Institution Name.
constexpr dicom_tag study_description
Study Description.
constexpr dicom_tag institution_address
Institution Address.
constexpr dicom_tag name_of_physicians_reading_study
Name of Physician(s) Reading Study.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag frame_of_reference_uid
Frame of Reference UID.
constexpr dicom_tag sop_instance_uid
SOP Instance UID.
constexpr dicom_tag patient_age
Patient's Age.
constexpr dicom_tag patient_address
Patient's Address.
constexpr dicom_tag patient_size
Patient's Size.
constexpr dicom_tag acquisition_date
Acquisition Date.
constexpr dicom_tag patient_birth_date
Patient's Birth Date.
constexpr dicom_tag scheduled_performing_physician_name
Scheduled Performing Physician's Name.
constexpr dicom_tag accession_number
Accession Number.
constexpr dicom_tag series_date
Series Date.
constexpr dicom_tag patient_sex
Patient's Sex.
constexpr dicom_tag study_instance_uid
Study Instance UID.
constexpr dicom_tag station_name
Station Name.
constexpr dicom_tag operators_name
Operators' Name.
constexpr dicom_tag performing_physician_name
Performing Physician's Name.
constexpr dicom_tag instance_creation_date
Instance Creation Date.
constexpr dicom_tag series_description
Series Description.
constexpr dicom_tag content_date
Content Date.
constexpr dicom_tag study_id
Study ID.
constexpr dicom_tag patient_name
Patient's Name.
constexpr dicom_tag study_date
Study Date.
constexpr dicom_tag series_instance_uid
Series Instance UID.
constexpr dicom_tag patient_weight
Patient's Weight.
auto get_all_identifier_tags() -> std::vector< core::dicom_tag >
Get all HIPAA identifier tags.
constexpr auto to_string(anonymization_profile profile) noexcept -> std::string_view
Convert profile enum to string representation.
anonymization_profile
DICOM de-identification profiles based on PS3.15 Annex E.
@ clean_pixel
Clean Pixel Data - Remove burned-in annotations.
@ hipaa_safe_harbor
HIPAA Safe Harbor - 18 identifier removal.
@ gdpr_compliant
GDPR Compliant - European data protection.
@ retain_longitudinal
Retain Longitudinal - Preserve temporal relationships.
@ retain_patient_characteristics
Retain Patient Characteristics.
@ clean_descriptions
Clean Descriptions - Sanitize text fields.
@ basic
Basic Profile - Remove direct identifiers.
@ hash
Hash the value for research linkage.
@ remove
D - Remove the attribute entirely.
@ keep
K - Keep the attribute unchanged.
@ replace_uid
U - Replace UIDs with new values.
@ shift_date
Shift dates by a fixed offset.
@ remove_or_empty
X - Remove or empty based on presence.
@ replace
C - Clean (replace with dummy value)
@ empty
Z - Replace with zero-length value.
private_tag_action
Action to take on private tags during anonymization.
Definition anonymizer.h:49
@ keep
Preserve all private tags (default for backward compatibility)
@ remove_data
Remove private data elements but keep creators (for auditing)
@ remove_all
Remove all private data elements and their creators.
Report generated after anonymization.
Definition tag_action.h:233
std::string profile_name
Profile used for anonymization.
Definition tag_action.h:265
Configuration for a custom tag action.
Definition tag_action.h:148
static auto make_remove() -> tag_action_config
Create a remove action config.
Definition tag_action.h:164
static auto make_hash(std::string algorithm="SHA256", bool salt=true) -> tag_action_config
Create a hash action config.
Definition tag_action.h:195
static auto make_keep() -> tag_action_config
Create a keep action config.
Definition tag_action.h:178
static auto make_replace(std::string value) -> tag_action_config
Create a replace action config with a custom value.
Definition tag_action.h:186
static auto make_empty() -> tag_action_config
Create an empty action config.
Definition tag_action.h:171
Record of an action performed on a tag.
Definition tag_action.h:210