wsi-dicom-annotations 0.2.1

DICOM WSI ANN, SEG, SR, and Parametric Map interoperability
Documentation
use super::{
    annotation_class_concept_key, dicom_cielab_to_srgb, srgb_to_dicom_cielab,
    AnnotationClassGeometry, AnnotationScheme, DicomCode, TrackingIdentity,
};

#[test]
fn public_concept_key_ignores_display_meaning_but_keeps_geometry() {
    let category = DicomCode::new("49755003", "SCT", "First display meaning").unwrap();
    let property = DicomCode::new("108369006", "SCT", "Neoplasm").unwrap();
    let renamed_category =
        DicomCode::new("49755003", "SCT", "A different display meaning").unwrap();

    let region =
        annotation_class_concept_key(AnnotationClassGeometry::Region, &category, &property, &[]);
    let renamed = annotation_class_concept_key(
        AnnotationClassGeometry::Region,
        &renamed_category,
        &property,
        &[],
    );
    let point =
        annotation_class_concept_key(AnnotationClassGeometry::Point, &category, &property, &[]);

    assert_eq!(region, renamed);
    assert_ne!(region, point);
}

#[test]
fn general_pathology_scheme_is_exact_qualified_and_roundtrips() {
    let scheme = AnnotationScheme::general_pathology_v1();

    assert_eq!(scheme.schema_version(), 1);
    assert_eq!(scheme.id(), "org.frames.general-pathology");
    assert_eq!(scheme.version(), 1);
    assert_eq!(scheme.display_name(), "General Pathology");
    assert_eq!(scheme.classes().len(), 8);

    let expected = [
        (
            "tissue",
            "Tissue",
            AnnotationClassGeometry::Region,
            [0x99, 0x99, 0x99],
            "85756007",
            "85756007",
        ),
        (
            "neoplasm",
            "Neoplasm",
            AnnotationClassGeometry::Region,
            [0xD5, 0x5E, 0x00],
            "49755003",
            "108369006",
        ),
        (
            "necrosis",
            "Necrosis",
            AnnotationClassGeometry::Region,
            [0xE6, 0x9F, 0x00],
            "49755003",
            "6574001",
        ),
        (
            "inflammation",
            "Inflammation",
            AnnotationClassGeometry::Region,
            [0xF0, 0xE4, 0x42],
            "49755003",
            "409774005",
        ),
        (
            "stroma",
            "Stroma / connective tissue",
            AnnotationClassGeometry::Region,
            [0x00, 0x9E, 0x73],
            "85756007",
            "21793004",
        ),
        (
            "unusable-tissue",
            "Unusable tissue",
            AnnotationClassGeometry::Region,
            [0xCC, 0x79, 0xA7],
            "263496004",
            "131502",
        ),
        (
            "cell",
            "Cell",
            AnnotationClassGeometry::Point,
            [0x56, 0xB4, 0xE9],
            "4421005",
            "4421005",
        ),
        (
            "nucleus",
            "Nucleus",
            AnnotationClassGeometry::Point,
            [0x00, 0x72, 0xB2],
            "4421005",
            "84640000",
        ),
    ];
    for (index, (class, expected)) in scheme.classes().iter().zip(expected).enumerate() {
        assert_eq!(class.id(), expected.0);
        assert_eq!(class.label(), expected.1);
        assert_eq!(class.geometry(), expected.2);
        assert_eq!(class.display_color(), expected.3);
        assert_eq!(class.category().value(), expected.4);
        assert_eq!(class.property_type().value(), expected.5);
        if index < 6 {
            assert_eq!(class.category().mapping_resource(), Some("DCMR"));
            assert_eq!(class.category().context_identifier(), Some("7150"));
        } else {
            assert_eq!(class.category().mapping_resource(), None);
            assert_eq!(class.category().context_identifier(), None);
        }
        assert_eq!(
            class.property_type().mapping_resource_uid(),
            Some("1.2.840.10008.8.1.1")
        );
        assert!(class.property_type().context_identifier().is_some());
    }

    let json = scheme.to_json().expect("built-in scheme should serialize");
    let restored = AnnotationScheme::from_json(&json).expect("built-in scheme should parse");
    assert_eq!(restored, scheme);
    assert_eq!(restored.content_digest(), scheme.content_digest());
}

#[test]
fn scheme_parsing_is_strict_bounded_and_rejects_junk_classes() {
    let duplicate_ids = br##"{
      "schema_version":1,"scheme_id":"org.example.test","scheme_version":1,
      "display_name":"Test","classes":[
        {"id":"same","label":"Tissue","geometry":"REGION","display_color":"#999999",
         "category":{"code_value":"85756007","coding_scheme_designator":"SCT","code_meaning":"Tissue"},
         "property_type":{"code_value":"85756007","coding_scheme_designator":"SCT","code_meaning":"Tissue"}},
        {"id":"same","label":"Neoplasm","geometry":"REGION","display_color":"#D55E00",
         "category":{"code_value":"49755003","coding_scheme_designator":"SCT","code_meaning":"Morphologically Abnormal Structure"},
         "property_type":{"code_value":"108369006","coding_scheme_designator":"SCT","code_meaning":"Neoplasm"}}
      ]
    }"##;
    let error = AnnotationScheme::from_json(duplicate_ids).expect_err("duplicate class IDs");
    assert!(error.to_string().contains("duplicate class id"));

    let duplicate_concepts = br##"{
      "schema_version":1,"scheme_id":"org.example.test","scheme_version":1,
      "display_name":"Test","classes":[
        {"id":"first","label":"Tissue","geometry":"REGION","display_color":"#999999",
         "category":{"code_value":"85756007","coding_scheme_designator":"SCT","code_meaning":"Tissue"},
         "property_type":{"code_value":"85756007","coding_scheme_designator":"SCT","code_meaning":"Tissue"}},
        {"id":"second","label":"Renamed","geometry":"REGION","display_color":"#FFFFFF",
         "category":{"code_value":"85756007","coding_scheme_designator":"SCT","code_meaning":"Different display meaning"},
         "property_type":{"code_value":"85756007","coding_scheme_designator":"SCT","code_meaning":"Different display meaning"}}
      ]
    }"##;
    let error = AnnotationScheme::from_json(duplicate_concepts).expect_err("duplicate concepts");
    assert!(error.to_string().contains("duplicate class concept"));

    let unknown = br##"{
      "schema_version":1,"scheme_id":"org.example.test","scheme_version":1,
      "display_name":"Test","classes":[],"new_class_button":true
    }"##;
    let error = AnnotationScheme::from_json(unknown).expect_err("unknown fields");
    assert!(error.to_string().contains("not valid JSON"));

    let oversized = vec![b' '; 4 * 1024 * 1024 + 1];
    let error = AnnotationScheme::from_json(&oversized).expect_err("oversized scheme");
    assert!(error.to_string().contains("4 MiB"));
}

#[test]
fn concept_identity_ignores_presentation_but_content_digest_does_not() {
    let first = br##"{
      "schema_version":1,"scheme_id":"org.example.first","scheme_version":1,
      "display_name":"First","classes":[
        {"id":"tissue","label":"Tissue","geometry":"REGION","display_color":"#999999",
         "category":{"code_value":"85756007","coding_scheme_designator":"SCT","code_meaning":"Tissue"},
         "property_type":{"code_value":"85756007","coding_scheme_designator":"SCT","code_meaning":"Tissue"}}
      ]
    }"##;
    let second = br##"{
      "schema_version":1,"scheme_id":"org.example.second","scheme_version":4,
      "display_name":"Second","classes":[
        {"id":"gray","label":"Renamed tissue","geometry":"REGION","display_color":"#FFFFFF",
         "category":{"code_value":"85756007","coding_scheme_designator":"SCT","code_meaning":"Alternate wording"},
         "property_type":{"code_value":"85756007","coding_scheme_designator":"SCT","code_meaning":"Alternate wording"}}
      ]
    }"##;
    let first = AnnotationScheme::from_json(first).expect("first scheme");
    let second = AnnotationScheme::from_json(second).expect("second scheme");

    assert_eq!(
        first.classes()[0].concept_key(),
        second.classes()[0].concept_key()
    );
    assert_ne!(first.content_digest(), second.content_digest());
}

#[test]
fn dicom_cielab_roundtrips_builtin_colors_within_two_code_levels() {
    for rgb in [
        [0x99, 0x99, 0x99],
        [0xD5, 0x5E, 0x00],
        [0xE6, 0x9F, 0x00],
        [0xF0, 0xE4, 0x42],
        [0x00, 0x9E, 0x73],
        [0xCC, 0x79, 0xA7],
        [0x56, 0xB4, 0xE9],
        [0x00, 0x72, 0xB2],
    ] {
        let restored = dicom_cielab_to_srgb(srgb_to_dicom_cielab(rgb));
        for (actual, expected) in restored.into_iter().zip(rgb) {
            assert!(
                actual.abs_diff(expected) <= 2,
                "{rgb:?} became {restored:?}"
            );
        }
    }
}

#[test]
fn tracking_identity_is_assigned_once_and_is_dicom_valid() {
    let identity = TrackingIdentity::generated("F", 7).expect("generated identity");
    assert_eq!(identity.id(), "F-000007");
    assert!(identity.uid().starts_with("2.25."));
    assert!(identity.uid().len() <= 64);

    let restored = TrackingIdentity::new(identity.id(), identity.uid()).expect("valid identity");
    assert_eq!(restored, identity);
    let error = TrackingIdentity::new("F-1", "not-a-uid").expect_err("invalid UID");
    assert!(error.to_string().contains("tracking UID"));
}