{"id":15654176,"url":"https://github.com/thibaudcolas/kontrasto","last_synced_at":"2025-04-30T08:22:07.721Z","repository":{"id":43224927,"uuid":"363797158","full_name":"thibaudcolas/kontrasto","owner":"thibaudcolas","description":"🎨  Automated color contrast for text over images","archived":false,"fork":false,"pushed_at":"2024-01-20T03:10:55.000Z","size":9958,"stargazers_count":27,"open_issues_count":13,"forks_count":3,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-25T19:17:30.323Z","etag":null,"topics":["color-thief","contrast-ratio","django","dominant-colors","numpy","pillow","wagtail","wcag2","wcag3"],"latest_commit_sha":null,"homepage":"https://kontrasto.netlify.app","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thibaudcolas.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-05-03T02:27:27.000Z","updated_at":"2024-05-29T08:47:09.000Z","dependencies_parsed_at":"2024-10-23T07:32:49.222Z","dependency_job_id":null,"html_url":"https://github.com/thibaudcolas/kontrasto","commit_stats":{"total_commits":38,"total_committers":2,"mean_commits":19.0,"dds":0.1842105263157895,"last_synced_commit":"267397dce25d298ec03d720fd464dfc72e7d04fe"},"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thibaudcolas%2Fkontrasto","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thibaudcolas%2Fkontrasto/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thibaudcolas%2Fkontrasto/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thibaudcolas%2Fkontrasto/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thibaudcolas","download_url":"https://codeload.github.com/thibaudcolas/kontrasto/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251666683,"owners_count":21624353,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["color-thief","contrast-ratio","django","dominant-colors","numpy","pillow","wagtail","wcag2","wcag3"],"created_at":"2024-10-03T12:49:50.048Z","updated_at":"2025-04-30T08:22:07.659Z","avatar_url":"https://github.com/thibaudcolas.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# [Kontrasto](https://kontrasto.netlify.app) \u003cimg src=\"https://raw.githubusercontent.com/thibaudcolas/kontrasto/main/.github/kontrasto-logo.svg?sanitize=true\" width=\"100\" height=\"100\" align=\"right\" alt=\"\"\u003e\n\n[![PyPI](https://img.shields.io/pypi/v/kontrasto.svg)](https://pypi.org/project/kontrasto/) [![npm](https://img.shields.io/npm/v/kontrasto.svg)](https://www.npmjs.com/package/kontrasto) [![PyPI downloads](https://img.shields.io/pypi/dm/kontrasto.svg)](https://pypi.org/project/kontrasto/) [![Build status](https://github.com/thibaudcolas/kontrasto/workflows/CI/badge.svg)](https://github.com/thibaudcolas/kontrasto/actions) [![Coverage Status](https://coveralls.io/repos/github/thibaudcolas/kontrasto/badge.svg?branch=main)](https://coveralls.io/github/thibaudcolas/kontrasto?branch=main) [![Total alerts](https://img.shields.io/lgtm/alerts/g/thibaudcolas/kontrasto.svg?logo=lgtm\u0026logoWidth=18)](https://lgtm.com/projects/g/thibaudcolas/kontrasto/alerts/) [![Netlify Status](https://api.netlify.com/api/v1/badges/e7517da4-87da-46d4-856e-13d5c2969908/deploy-status)](https://app.netlify.com/sites/kontrasto/deploys)\n\n[![🎨 Automated color contrast for text over images](https://raw.githubusercontent.com/thibaudcolas/kontrasto/main/.github/repository-social-media.jpg)](https://kontrasto.netlify.app)\n\n## Why we need this\n\nKontrasto is a dual Python and JavaScript library which analyses instances of text over images, and transforms the text to make it more readable and have a [higher contrast](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html) against the background.\n\nUsing Kontrasto both server-side and client-side gives the best results: server-side processing means users will have the best possible styles as the page loads, while client-side processing can refine the result based on the final position of the text over the image.\n\nHere is a demo, over different areas of an image, with different methods of dominant color extraction and contrast ratio calculation:\n\n[![Snow-covered landscape with snow-covered trees, blue-white sky, and a snow-covered radio tower on the horizon](https://raw.githubusercontent.com/thibaudcolas/kontrasto/main/.github/kontrasto-demo-readme.jpg)](https://kontrasto.netlify.app/)\n\nCheck out our [live demo](https://kontrasto.netlify.app/) for other examples.\n\n## Usage in Python\n\nkontrasto is available on [PyPI](https://pypi.org/project/kontrasto/).\n\n```bash\n# Assuming you’re using Python 3.6+,\npip install kontrasto\n```\n\nThe simplest way to try it out is to use the command-line interface:\n\n```bash\nkontrasto --text '#00ff00' demo_images/blue-sky-cliffs.jpg\n```\n\nThis will extract the image’s dominant color, and compare it against three text colors: white, black, and the provided #00ff00. Here is a sample result:\n\n- Dominant color: `#4971a1` (https://whocanuse.com/?b=4971a1\u0026c=\u0026f=16)\n- WCAG 2 contrast black: 4.16:1 (AA large only, https://whocanuse.com/?b=4971a1\u0026c=000000\u0026f=16)\n- WCAG 2 contrast white: 5.05:1 (AA, AAA large, https://whocanuse.com/?b=4971a1\u0026c=ffffff\u0026f=16)\n- WCAG 2 contrast text color: 3.68:1 (AA large only, https://whocanuse.com/?b=4971a1\u0026c=00ff00\u0026f=16)\n- WCAG 3 contrast black: 29.158302335633827\n- https://whocanuse.com/?b=4971a1\u0026c=000000\u0026f=16\n- WCAG 3 contrast white: 82.7306051896947\n- [(18, 400), (16, 500)]\n- https://www.myndex.com/APCA/?BG=4971a1\u0026TXT=ffffff\n- https://whocanuse.com/?b=4971a1\u0026c=ffffff\u0026f=18\n- https://whocanuse.com/?b=4971a1\u0026c=ffffff\u0026f=16\n- WCAG 3 contrast text color: 60.98703767172198\n- [(24, 400), (18, 500), (16, 600)]\n- https://www.myndex.com/APCA/?BG=4971a1\u0026TXT=00ff00\n- https://whocanuse.com/?b=4971a1\u0026c=00ff00\u0026f=24\n- https://whocanuse.com/?b=4971a1\u0026c=00ff00\u0026f=18\n- https://whocanuse.com/?b=4971a1\u0026c=00ff00\u0026f=16\u0026s=b\n\nFrom there, we can move onto more serious use cases!\n\n### Usage in vanilla Python\n\nImport the low-level methods and enjoy:\n\n```python\nfrom kontrasto import wcag_2, wcag_3\nfrom kontrasto.convert import to_hex\nfrom kontrasto.contrast import get_dominant_color\nfrom PIL import Image\n\ndef wcag_2_contrast_light_or_dark(\n    image, light_color: str, dark_color: str\n) -\u003e Dict[str, str]:\n    dominant = to_hex(get_dominant_color(image))\n    light_contrast = wcag_2.wcag2_contrast(dominant, light_color)\n    dark_contrast = wcag_2.wcag2_contrast(dominant, dark_color)\n    lighter = light_contrast \u003e dark_contrast\n    return {\n        \"text_color\": light_color if lighter else dark_color,\n        \"text_theme\": \"light\" if lighter else \"dark\",\n        \"bg_color\": dominant,\n        \"bg_theme\": \"dark\" if lighter else \"light\",\n    }\n\ndef wcag_3_contrast_light_or_dark(\n    image, light_color: str, dark_color: str\n) -\u003e Dict[str, str]:\n    dominant = to_hex(get_dominant_color(image))\n    light_contrast = wcag_3.format_contrast(\n        wcag_3.apca_contrast(dominant, light_color)\n    )\n    dark_contrast = wcag_3.format_contrast(\n        wcag_3.apca_contrast(dominant, dark_color)\n    )\n    lighter = light_contrast \u003e dark_contrast\n    return {\n        \"text_color\": light_color if lighter else dark_color,\n        \"text_theme\": \"light\" if lighter else \"dark\",\n        \"bg_color\": dominant,\n        \"bg_theme\": \"dark\" if lighter else \"light\",\n    }\n```\n\n### Usage in Wagtail\n\nIn Wagtail, using Kontrasto is much simpler:\n\n- The result of dominant color extraction is cached, greatly improving performance.\n- The above methods are directly available as template tags.\n\nAt least for now, this does mean using Kontrasto requires adding a field to a [custom image model](https://docs.wagtail.org/en/stable/advanced_topics/images/custom_image_model.html):\n\n```python\nfrom wagtail.images.models import (\n    AbstractImage,\n    AbstractRendition,\n    SourceImageIOError,\n)\n\nfrom kontrasto.willow_operations import pillow_dominant\n\nclass CustomImage(AbstractImage):\n    dominant_color = models.CharField(max_length=10, blank=True)\n\n    admin_form_fields = Image.admin_form_fields\n\n    def has_dominant_color(self) -\u003e bool:\n        return self.dominant_color\n\n    def set_dominant_color(self, color: str) -\u003e bool:\n        self.dominant_color = color\n\n    def get_dominant_color(self):\n        if not self.has_dominant_color():\n            with self.get_willow_image() as willow:\n                try:\n                    self.dominant_color = pillow_dominant(willow)\n                except Exception as e:\n                    # File not found\n                    #\n                    # Have to catch everything, because the exception\n                    # depends on the file subclass, and therefore the\n                    # storage being used.\n                    raise SourceImageIOError(str(e))\n\n                self.save(update_fields=[\"dominant_color\"])\n\n        return self.dominant_color\n\n\nclass Rendition(AbstractRendition):\n    image = models.ForeignKey(\n        \"CustomImage\", related_name=\"renditions\", on_delete=models.CASCADE\n    )\n\n    class Meta:\n        unique_together = ((\"image\", \"filter_spec\", \"focal_point_key\"),)\n```\n\nThen, in templates:\n\n```html\n{% wcag_2_contrast_light_or_dark page.test_image \"#ffffff\" \"#000000\" as result\n%} {% wcag_3_contrast_light_or_dark page.test_image \"#ffffff\" \"#000000\" as\nresult_3 %}\n\u003cdiv\n  data-banner\n  style=\"--kontrasto-text:{{ result_3.text_color }}; --kontrasto-bg:{{ result_3.bg_color }}99;\"\n\u003e\n  {% image page.test_image width-800 loading=\"lazy\" data-banner-image=\"true\" %}\n  \u003cp\u003e\n    \u003cspan class=\"kontrasto-text-bg\" data-banner-text\u003e{{ demo_text }}\u003c/span\u003e\n  \u003c/p\u003e\n\u003c/div\u003e\n```\n\nThis additionally relies on the following CSS, for the simplest integration with client-side processing:\n\n```css\n.kontrasto-text-bg {\n  color: var(--kontrasto-text);\n  background: var(--kontrasto-bg);\n}\n```\n\n## Usage in JavaScript\n\nkontrasto is available on [npm](https://www.npmjs.com/package/kontrasto) for client-side (browser) JavaScript. This option makes it possible to only extract the dominant color of images _where the text appears_, which leads to much better results. However, it has the caveat of executing in the users’ browser, which has a clear performance cost.\n\n```bash\nnpm install kontrasto\n```\n\nUsing it in JavaScript is slightly different.\n\n### Vanilla JavaScript\n\nHere is a basic vanilla JavaScript integration:\n\n```js\nimport {\n  wcag_2_contrast_light_or_dark,\n  wcag_3_contrast_light_or_dark,\n} from \"kontrasto\";\n\nconst banner = document.querySelector(\"[data-banner]\");\nconst bannerImage = banner.querySelector(\"[data-banner-image]\");\nconst bannerText = banner.querySelector(\"[data-banner-text]\");\n\nconst contrast = wcag_3_contrast_light_or_dark(\n  bannerImage,\n  \"#ffffff\",\n  \"#000000\",\n  bannerText,\n);\nbanner.style.setProperty(\"--kontrasto-bg\", `${contrast.bg_color}99`);\nbanner.style.setProperty(\"--kontrasto-text\", contrast.text_color);\n```\n\nThis assumes an HTML structure like:\n\n```html\n{% wcag_2_contrast_light_or_dark page.test_image \"#ffffff\" \"#000000\" as result\n%} {% wcag_3_contrast_light_or_dark page.test_image \"#ffffff\" \"#000000\" as\nresult_3 %}\n\u003cdiv\n  data-banner\n  style=\"--kontrasto-text:{{ result_3.text_color }}; --kontrasto-bg:{{ result_3.bg_color }}99;\"\n\u003e\n  {% image page.test_image width-800 loading=\"lazy\" data-banner-image=\"true\" %}\n  \u003cp\u003e\n    \u003cspan class=\"kontrasto-text-bg\" data-banner-text\u003e{{ demo_text }}\u003c/span\u003e\n  \u003c/p\u003e\n\u003c/div\u003e\n```\n\nAnd CSS:\n\n```css\n.kontrasto-text-bg {\n  color: var(--kontrasto-text);\n  background: var(--kontrasto-bg);\n}\n```\n\nCombined with the server-side integration, this makes it possible to deliver both the best possible performance, and the best possible text contrast enhancement.\n\n#### Particular considerations\n\nTODO\n\n### React\n\nTODO\n\n## Contributing\n\nSee anything you like in here? Anything missing? We welcome all support, whether on bug reports, feature requests, code, design, reviews, tests, documentation, and more. Please have a look at our [contribution guidelines](CONTRIBUTING.md).\n\nIf you just want to set up the project on your own computer, the contribution guidelines also contain all of the setup commands.\n\n## Credits\n\nImage credit: [FxEmojis](https://github.com/mozilla/fxemoji). [Test templates](tests/README.md) extracted from third-party projects. Website hosted by [Netlify](https://www.netlify.com/).\n\nView the full list of [contributors](https://github.com/thibaudcolas/kontrasto/graphs/contributors). [MIT](LICENSE) licensed. Website content available as [CC0](https://creativecommons.org/share-your-work/public-domain/cc0/).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthibaudcolas%2Fkontrasto","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthibaudcolas%2Fkontrasto","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthibaudcolas%2Fkontrasto/lists"}