///|
/// Severity of a validation issue.
pub(all) enum ValidationSeverity {
Error
Warning
} derive(Eq, Debug)
///|
/// One structural or semantic validation finding.
pub(all) struct ValidationIssue {
severity : ValidationSeverity
code : String
tag : Tag?
message : String
} derive(Eq, Debug)
///|
/// The complete validation result for a data set.
pub(all) struct ValidationReport {
issues : Array[ValidationIssue]
} derive(Eq, Debug)
///|
/// Validate a DICOM data set without modifying it.
pub fn validate(dataset : DataSet) -> ValidationReport {
let issues : Array[ValidationIssue] = []
check_required_text(
dataset,
issues,
sop_class_uid(),
"META001",
"SOP Class UID is missing",
)
check_required_text(
dataset,
issues,
sop_instance_uid(),
"META002",
"SOP Instance UID is missing",
)
check_required_text(
dataset,
issues,
study_instance_uid(),
"META003",
"Study Instance UID is missing",
)
check_required_text(
dataset,
issues,
series_instance_uid(),
"META004",
"Series Instance UID is missing",
)
check_required_text(
dataset,
issues,
modality(),
"META005",
"Modality is missing",
)
check_uid(dataset, issues, sop_class_uid(), "UID001")
check_uid(dataset, issues, sop_instance_uid(), "UID002")
check_uid(dataset, issues, study_instance_uid(), "UID003")
check_uid(dataset, issues, series_instance_uid(), "UID004")
check_date(dataset, issues, study_date(), "DATE001")
check_positive(dataset, issues, rows(), "IMAGE001", "Rows must be positive")
check_positive(
dataset,
issues,
columns(),
"IMAGE002",
"Columns must be positive",
)
check_pixel_representation(dataset, issues)
check_bits(dataset, issues)
check_pixel_data(dataset, issues)
{ issues, }
}
///|
/// Return all findings in insertion order.
pub fn ValidationReport::issues(
self : ValidationReport,
) -> Array[ValidationIssue] {
self.issues.copy()
}
///|
/// Return the number of errors.
pub fn ValidationReport::error_count(self : ValidationReport) -> Int {
self.count(Error)
}
///|
/// Return the number of warnings.
pub fn ValidationReport::warning_count(self : ValidationReport) -> Int {
self.count(Warning)
}
///|
fn ValidationReport::count(
self : ValidationReport,
severity : ValidationSeverity,
) -> Int {
let mut count = 0
for issue in self.issues {
if issue.severity == severity {
count += 1
}
}
count
}
///|
/// Return the severity of an issue.
pub fn ValidationIssue::severity(self : ValidationIssue) -> ValidationSeverity {
self.severity
}
///|
/// Return the stable machine-readable issue code.
pub fn ValidationIssue::code(self : ValidationIssue) -> String {
self.code
}
///|
/// Return the related tag, when one exists.
pub fn ValidationIssue::tag(self : ValidationIssue) -> Tag? {
self.tag
}
///|
/// Return the human-readable issue message.
pub fn ValidationIssue::message(self : ValidationIssue) -> String {
self.message
}
///|
fn add_issue(
issues : Array[ValidationIssue],
severity : ValidationSeverity,
code : String,
tag : Tag?,
message : String,
) -> Unit {
issues.push({ severity, code, tag, message })
}
///|
fn check_required_text(
dataset : DataSet,
issues : Array[ValidationIssue],
tag : Tag,
code : String,
message : String,
) -> Unit {
match dataset.get_text(tag) {
Some(value) if value != "" => ()
_ => add_issue(issues, Error, code, Some(tag), message)
}
}
///|
fn check_uid(
dataset : DataSet,
issues : Array[ValidationIssue],
tag : Tag,
code : String,
) -> Unit {
match dataset.get_text(tag) {
None => ()
Some(value) if valid_uid(value) => ()
Some(_) =>
add_issue(
issues,
Error,
code,
Some(tag),
"UID must contain only digits and dots and must not be empty",
)
}
}
///|
fn valid_uid(value : String) -> Bool {
if value == "" ||
value.length() > 64 ||
value.has_prefix(".") ||
value.has_suffix(".") {
false
} else {
let mut previous_dot = false
let mut valid = true
for char in value {
if char == '.' {
if previous_dot {
valid = false
}
previous_dot = true
} else if char >= '0' && char <= '9' {
previous_dot = false
} else {
valid = false
}
}
valid
}
}
///|
fn check_date(
dataset : DataSet,
issues : Array[ValidationIssue],
tag : Tag,
code : String,
) -> Unit {
match dataset.get_text(tag) {
None => ()
Some(value) if valid_date(value) => ()
Some(_) =>
add_issue(
issues,
Warning,
code,
Some(tag),
"Date must use DICOM DA format YYYYMMDD",
)
}
}
///|
fn valid_date(value : String) -> Bool {
if value.length() != 8 {
false
} else {
let mut valid = true
for char in value {
if char < '0' || char > '9' {
valid = false
}
}
valid
}
}
///|
fn check_positive(
dataset : DataSet,
issues : Array[ValidationIssue],
tag : Tag,
code : String,
message : String,
) -> Unit {
match dataset.get_int(tag) {
None => add_issue(issues, Warning, code, Some(tag), message)
Some(value) if value > 0 => ()
Some(_) => add_issue(issues, Error, code, Some(tag), message)
}
}
///|
fn check_pixel_representation(
dataset : DataSet,
issues : Array[ValidationIssue],
) -> Unit {
match dataset.get_int(pixel_representation()) {
None => ()
Some(0) | Some(1) => ()
Some(_) =>
add_issue(
issues,
Error,
"IMAGE003",
Some(pixel_representation()),
"Pixel Representation must be 0 or 1",
)
}
}
///|
fn check_bits(dataset : DataSet, issues : Array[ValidationIssue]) -> Unit {
let allocated = dataset.get_int(bits_allocated())
let stored = dataset.get_int(bits_stored())
match allocated {
Some(allocated_value) =>
match stored {
Some(stored_value) if stored_value <= allocated_value &&
stored_value > 0 => ()
Some(_) =>
add_issue(
issues,
Error,
"IMAGE004",
Some(bits_stored()),
"Bits Stored must be positive and no greater than Bits Allocated",
)
None => ()
}
None => ()
}
}
///|
fn check_pixel_data(dataset : DataSet, issues : Array[ValidationIssue]) -> Unit {
let has_dimensions = dataset.get_int(rows()) is Some(_) ||
dataset.get_int(columns()) is Some(_)
if has_dimensions && dataset.get(pixel_data()) is None {
add_issue(
issues,
Warning,
"PIXEL001",
Some(pixel_data()),
"Pixel Data is missing while image dimensions are present",
)
}
}