Support adding an ignore reason
diff --git a/sparse_strips/vello_dev_macros/src/lib.rs b/sparse_strips/vello_dev_macros/src/lib.rs
index 70b5c18..bec3f15 100644
--- a/sparse_strips/vello_dev_macros/src/lib.rs
+++ b/sparse_strips/vello_dev_macros/src/lib.rs
@@ -28,18 +28,13 @@
     Flag(Ident),
 }
 
-fn is_flag(key: &Ident) -> bool {
-    matches!(
-        key.to_string().as_str(),
-        "transparent" | "skip_cpu" | "skip_hybrid" | "no_ref" | "ignore"
-    )
-}
-
 impl Parse for Attribute {
     fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
         let key = input.parse()?;
 
-        if is_flag(&key) {
+        let is_flag = !input.peek(Token![=]);
+        
+        if is_flag {
             Ok(Self::Flag(key))
         } else {
             // Skip equality token.
@@ -85,6 +80,8 @@
     /// Whether no reference image should actually be created (for tests that only check
     /// for panics, but are not interested in the actual output).
     no_ref: bool,
+    /// A reason for ignoring a test.
+    ignore_reason: Option<String>,
 }
 
 impl Default for Arguments {
@@ -98,6 +95,7 @@
             skip_cpu: false,
             skip_hybrid: false,
             no_ref: false,
+            ignore_reason: None,
         }
     }
 }
@@ -146,6 +144,7 @@
         transparent,
         skip_cpu,
         mut skip_hybrid,
+        ignore_reason,
         no_ref,
     } = parse_args(&attrs);
 
@@ -162,18 +161,24 @@
     };
 
     let empty_snippet = quote! {};
-    let ignore_snippet = quote! {#[ignore]};
+    let ignore_snippet = if let Some(reason) = ignore_reason {
+        quote! {#[ignore = #reason]}
+    }   else {
+        quote! {#[ignore]}
+    };
 
     let ignore_hybrid = if skip_hybrid {
         ignore_snippet.clone()
     } else {
         empty_snippet.clone()
     };
+    
     let ignore_cpu = if skip_cpu {
         ignore_snippet.clone()
     } else {
         empty_snippet.clone()
     };
+    
     cpu_tolerance += DEFAULT_CPU_U8_TOLERANCE;
      hybrid_tolerance += DEFAULT_HYBRID_TOLERANCE;
 
@@ -222,6 +227,7 @@
             Attribute::KeyValue { key, expr, .. } => {
                 let key_str = key.to_string();
                 match key_str.as_str() {
+                    "ignore" => args.ignore_reason = Some(parse_string_lit(expr, "ignore")),
                     "width" => args.width = parse_int_lit(expr, "width"),
                     "height" => args.height = parse_int_lit(expr, "height"),
                     #[allow(clippy::cast_possible_truncation, reason = "user-supplied value")]
@@ -262,3 +268,15 @@
         panic!("invalid expression supplied to `{name}`")
     }
 }
+
+fn parse_string_lit(expr: &Expr, name: &str) -> String {
+    if let Expr::Lit(syn::ExprLit {
+        lit: syn::Lit::Str(lit_str),
+        ..
+    }) = expr
+    {
+        lit_str.value()
+    } else {
+        panic!("invalid expression supplied to `{name}`")
+    }
+}
diff --git a/sparse_strips/vello_sparse_tests/tests/image.rs b/sparse_strips/vello_sparse_tests/tests/image.rs
index bfe60b4..56f8a65 100644
--- a/sparse_strips/vello_sparse_tests/tests/image.rs
+++ b/sparse_strips/vello_sparse_tests/tests/image.rs
@@ -135,8 +135,7 @@
     );
 }
 
-// TODO: The below two test cases fail on Windows CI for some reason.
-#[vello_test(ignore)]
+#[vello_test(ignore = "fails in Windows CI for some reason.")]
 fn image_with_transform_rotate_1(ctx: &mut impl Renderer) {
     transform(
         ctx,
@@ -148,7 +147,7 @@
     );
 }
 
-#[vello_test(ignore)]
+#[vello_test(ignore = "fails in Windows CI for some reason.")]
 fn image_with_transform_rotate_2(ctx: &mut impl Renderer) {
     transform(
         ctx,