diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6629555..ae12b19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v3 @@ -22,19 +22,15 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Cache pip packages - uses: actions/cache@v3 + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} - restore-keys: | - ${{ runner.os }}-pip- + enable-cache: true - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install -e . - pip install -r requirements-dev.txt + uv pip install --system -e . + uv pip install --system -r requirements-dev.txt - name: Lint with flake8 run: | @@ -79,10 +75,12 @@ jobs: with: python-version: '3.10' + - name: Install uv + uses: astral-sh/setup-uv@v5 + - name: Install build dependencies run: | - python -m pip install --upgrade pip - pip install build twine + uv pip install --system build twine - name: Build package run: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7510274..7fa6b43 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,22 +39,45 @@ Enhancement suggestions are tracked as GitHub issues. When creating an enhanceme ## Development Setup +### Prerequisites + +- Python 3.10 or higher +- [uv](https://github.com/astral-sh/uv) package manager + +### Setup Steps + 1. Clone your fork: ```bash git clone https://github.com/yourusername/ContrastCheck.git cd ContrastCheck ``` -2. Create a virtual environment: +2. Install uv if you haven't already: ```bash -python -m venv venv -source venv/bin/activate # On Windows: venv\Scripts\activate +# On macOS and Linux +curl -LsSf https://astral.sh/uv/install.sh | sh + +# On Windows +powershell -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +3. Create a virtual environment with Python 3.10+: +```bash +uv venv --python 3.10 +``` + +4. Activate the virtual environment: +```bash +# On macOS/Linux: +source .venv/bin/activate + +# On Windows: +.venv\Scripts\activate ``` -3. Install development dependencies: +5. Install development dependencies: ```bash -pip install -e . -pip install -r requirements-dev.txt +uv pip install -e ".[dev]" ``` ## Coding Standards diff --git a/Makefile b/Makefile index 54a75ad..284a206 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,10 @@ -.PHONY: help install install-dev test test-cov lint format clean build upload docs +.PHONY: help venv install install-dev test test-cov lint format clean build upload docs help: @echo "ContrastCheck - Development Commands" @echo "" @echo "Available targets:" + @echo " venv - Create virtual environment with uv" @echo " install - Install package in production mode" @echo " install-dev - Install package with development dependencies" @echo " test - Run tests with pytest" @@ -15,12 +16,17 @@ help: @echo " upload - Upload package to PyPI" @echo " docs - Generate documentation" +venv: + uv venv --python 3.10 + @echo "Virtual environment created. Activate it with:" + @echo " source .venv/bin/activate (macOS/Linux)" + @echo " .venv\\Scripts\\activate (Windows)" + install: - pip install -e . + uv pip install -e . install-dev: - pip install -e . - pip install -r requirements-dev.txt + uv pip install -e ".[dev]" test: pytest diff --git a/README.md b/README.md index ea23b42..03bc111 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ContrastCheck -[![Python Version](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/) +[![Python Version](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) A powerful tool for analyzing text-background contrast ratios in UI screenshots using PaddleOCR and K-means clustering to ensure WCAG accessibility compliance. @@ -18,27 +18,51 @@ A powerful tool for analyzing text-background contrast ratios in UI screenshots ### Prerequisites -- Python 3.8 or higher -- pip package manager +- Python 3.10 or higher +- [uv](https://github.com/astral-sh/uv) - Fast Python package and project manager -### Install from source +### Install uv + +If you don't have uv installed: ```bash -git clone https://github.com/longweillw-blip/ContrastCheck.git -cd ContrastCheck -pip install -e . +# On macOS and Linux +curl -LsSf https://astral.sh/uv/install.sh | sh + +# On Windows +powershell -c "irm https://astral.sh/uv/install.ps1 | iex" + +# Or via pip +pip install uv ``` -### Install dependencies +### Install from source ```bash -pip install -r requirements.txt +git clone https://github.com/longway-code/ContrastCheck.git +cd ContrastCheck + +# Create virtual environment with uv (Python 3.10+) +uv venv --python 3.10 + +# Activate virtual environment +# On macOS/Linux: +source .venv/bin/activate +# On Windows: +.venv\Scripts\activate + +# Install the package +uv pip install -e . ``` -For development: +### Install dependencies ```bash -pip install -r requirements-dev.txt +# Production dependencies +uv pip install -r requirements.txt + +# Development dependencies +uv pip install -r requirements-dev.txt ``` ### GPU Support (Optional) @@ -46,8 +70,8 @@ pip install -r requirements-dev.txt For faster OCR processing with GPU: ```bash -pip uninstall paddlepaddle -pip install paddlepaddle-gpu +uv pip uninstall paddlepaddle +uv pip install paddlepaddle-gpu ``` ## Quick Start @@ -227,6 +251,19 @@ ContrastCheck/ ## Development +### Setup Development Environment + +```bash +# Create virtual environment with Python 3.10+ +uv venv --python 3.10 + +# Activate virtual environment +source .venv/bin/activate # On Windows: .venv\Scripts\activate + +# Install in development mode with dev dependencies +uv pip install -e ".[dev]" +``` + ### Running Tests Run all tests: @@ -310,7 +347,7 @@ If you use ContrastCheck in your research or project, please cite: title = {ContrastCheck: UI Screenshot Contrast Ratio Analyzer}, author = {ContrastCheck Contributors}, year = {2026}, - url = {https://github.com/longweillw-blip/ContrastCheck} + url = {https://github.com/longway-code/ContrastCheck} } ``` @@ -318,7 +355,7 @@ If you use ContrastCheck in your research or project, please cite: If you encounter any issues or have questions: -- Open an issue on [GitHub](https://github.com/yourusername/ContrastCheck/issues) +- Open an issue on [GitHub](https://github.com/longway-code/ContrastCheck/issues) - Check the [examples](examples/) directory for usage examples - Read the [WCAG 2.1 documentation](https://www.w3.org/WAI/WCAG21/quickref/) for accessibility guidelines diff --git a/contrast_check/__init__.py b/contrast_check/__init__.py index 3875a08..514199d 100644 --- a/contrast_check/__init__.py +++ b/contrast_check/__init__.py @@ -8,8 +8,8 @@ __version__ = "0.1.0" __author__ = "ContrastCheck Contributors" -from .ocr_extractor import OCRExtractor from .color_extractor import ColorExtractor from .contrast_checker import ContrastChecker +from .ocr_extractor import OCRExtractor __all__ = ["OCRExtractor", "ColorExtractor", "ContrastChecker"] diff --git a/contrast_check/color_extractor.py b/contrast_check/color_extractor.py index dc4dfca..330bfa5 100644 --- a/contrast_check/color_extractor.py +++ b/contrast_check/color_extractor.py @@ -2,10 +2,10 @@ Color extraction module using K-means clustering. """ -import numpy as np -from typing import Tuple, List -from PIL import Image +from typing import List, Tuple + import cv2 +import numpy as np from sklearn.cluster import KMeans @@ -26,9 +26,7 @@ def __init__(self, n_text_colors: int = 3, n_bg_colors: int = 3): self.n_bg_colors = n_bg_colors def extract_text_color( - self, - image: np.ndarray, - text_mask: np.ndarray + self, image: np.ndarray, text_mask: np.ndarray ) -> Tuple[int, int, int]: """ Extract dominant text color from the masked region. @@ -48,15 +46,14 @@ def extract_text_color( # Convert BGR to RGB text_pixels_rgb = cv2.cvtColor( - text_pixels.reshape(-1, 1, 3), - cv2.COLOR_BGR2RGB + text_pixels.reshape(-1, 1, 3), cv2.COLOR_BGR2RGB ).reshape(-1, 3) # Use K-means to find dominant colors kmeans = KMeans( n_clusters=min(self.n_text_colors, len(text_pixels)), random_state=42, - n_init=10 + n_init=10, ) kmeans.fit(text_pixels_rgb) @@ -73,7 +70,7 @@ def extract_background_color( image: np.ndarray, text_mask: np.ndarray, bbox: List[List[float]], - margin: int = 10 + margin: int = 10, ) -> Tuple[int, int, int]: """ Extract background color around the text region. @@ -109,15 +106,12 @@ def extract_background_color( # Convert BGR to RGB bg_pixels_rgb = cv2.cvtColor( - bg_pixels.reshape(-1, 1, 3), - cv2.COLOR_BGR2RGB + bg_pixels.reshape(-1, 1, 3), cv2.COLOR_BGR2RGB ).reshape(-1, 3) # Use K-means to find dominant colors kmeans = KMeans( - n_clusters=min(self.n_bg_colors, len(bg_pixels)), - random_state=42, - n_init=10 + n_clusters=min(self.n_bg_colors, len(bg_pixels)), random_state=42, n_init=10 ) kmeans.fit(bg_pixels_rgb) @@ -140,4 +134,4 @@ def rgb_to_hex(rgb: Tuple[int, int, int]) -> str: Returns: Hex color code string """ - return '#{:02x}{:02x}{:02x}'.format(int(rgb[0]), int(rgb[1]), int(rgb[2])) + return "#{:02x}{:02x}{:02x}".format(int(rgb[0]), int(rgb[1]), int(rgb[2])) diff --git a/contrast_check/contrast_checker.py b/contrast_check/contrast_checker.py index 2abb990..02b92ff 100644 --- a/contrast_check/contrast_checker.py +++ b/contrast_check/contrast_checker.py @@ -2,8 +2,8 @@ Contrast ratio calculation and WCAG compliance checking. """ -from typing import Tuple, Dict import math +from typing import Dict, Tuple class ContrastChecker: @@ -48,9 +48,7 @@ def gamma_correct(channel): @classmethod def calculate_contrast_ratio( - cls, - color1: Tuple[int, int, int], - color2: Tuple[int, int, int] + cls, color1: Tuple[int, int, int], color2: Tuple[int, int, int] ) -> float: """ Calculate contrast ratio between two colors. @@ -74,9 +72,7 @@ def calculate_contrast_ratio( @classmethod def check_wcag_compliance( - cls, - contrast_ratio: float, - is_large_text: bool = False + cls, contrast_ratio: float, is_large_text: bool = False ) -> Dict[str, bool]: """ Check WCAG compliance levels for a contrast ratio. @@ -95,11 +91,7 @@ def check_wcag_compliance( aa_pass = contrast_ratio >= cls.WCAG_AA_NORMAL aaa_pass = contrast_ratio >= cls.WCAG_AAA_NORMAL - return { - 'AA': aa_pass, - 'AAA': aaa_pass, - 'ratio': round(contrast_ratio, 2) - } + return {"AA": aa_pass, "AAA": aaa_pass, "ratio": round(contrast_ratio, 2)} @classmethod def get_contrast_level(cls, contrast_ratio: float) -> str: @@ -126,7 +118,7 @@ def analyze_contrast( cls, text_color: Tuple[int, int, int], bg_color: Tuple[int, int, int], - is_large_text: bool = False + is_large_text: bool = False, ) -> Dict: """ Perform complete contrast analysis. @@ -144,11 +136,11 @@ def analyze_contrast( level = cls.get_contrast_level(contrast_ratio) return { - 'text_color': text_color, - 'bg_color': bg_color, - 'contrast_ratio': round(contrast_ratio, 2), - 'wcag_aa': compliance['AA'], - 'wcag_aaa': compliance['AAA'], - 'level': level, - 'is_large_text': is_large_text + "text_color": text_color, + "bg_color": bg_color, + "contrast_ratio": round(contrast_ratio, 2), + "wcag_aa": compliance["AA"], + "wcag_aaa": compliance["AAA"], + "level": level, + "is_large_text": is_large_text, } diff --git a/contrast_check/main.py b/contrast_check/main.py index 6b346d4..6bf1932 100644 --- a/contrast_check/main.py +++ b/contrast_check/main.py @@ -6,13 +6,13 @@ import json import sys from pathlib import Path -from typing import List, Dict +from typing import Dict, List + import cv2 -import numpy as np -from .ocr_extractor import OCRExtractor from .color_extractor import ColorExtractor from .contrast_checker import ContrastChecker +from .ocr_extractor import OCRExtractor class ContrastAnalyzer: @@ -23,9 +23,9 @@ class ContrastAnalyzer: def __init__( self, use_gpu: bool = False, - lang: str = 'en', + lang: str = "en", n_text_colors: int = 3, - n_bg_colors: int = 3 + n_bg_colors: int = 3, ): """ Initialize the contrast analyzer. @@ -38,16 +38,11 @@ def __init__( """ self.ocr_extractor = OCRExtractor(use_gpu=use_gpu, lang=lang) self.color_extractor = ColorExtractor( - n_text_colors=n_text_colors, - n_bg_colors=n_bg_colors + n_text_colors=n_text_colors, n_bg_colors=n_bg_colors ) self.contrast_checker = ContrastChecker() - def analyze_image( - self, - image_path: str, - is_large_text: bool = False - ) -> List[Dict]: + def analyze_image(self, image_path: str, is_large_text: bool = False) -> List[Dict]: """ Analyze contrast ratios in an image. @@ -74,54 +69,42 @@ def analyze_image( for idx, region in enumerate(text_regions): # Create text mask text_mask = self.ocr_extractor.get_text_region_mask( - image_shape, - region['bbox'] + image_shape, region["bbox"] ) # Extract colors - text_color = self.color_extractor.extract_text_color( - image, - text_mask - ) + text_color = self.color_extractor.extract_text_color(image, text_mask) bg_color = self.color_extractor.extract_background_color( - image, - text_mask, - region['bbox'] + image, text_mask, region["bbox"] ) # Analyze contrast analysis = self.contrast_checker.analyze_contrast( - text_color, - bg_color, - is_large_text + text_color, bg_color, is_large_text ) # Add region info result = { - 'index': idx, - 'text': region['text'], - 'confidence': round(region['confidence'], 3), - 'bbox': region['bbox'], - 'center': region['center'], - 'text_color': text_color, - 'text_color_hex': self.color_extractor.rgb_to_hex(text_color), - 'bg_color': bg_color, - 'bg_color_hex': self.color_extractor.rgb_to_hex(bg_color), - 'contrast_ratio': analysis['contrast_ratio'], - 'wcag_aa': analysis['wcag_aa'], - 'wcag_aaa': analysis['wcag_aaa'], - 'level': analysis['level'] + "index": idx, + "text": region["text"], + "confidence": round(region["confidence"], 3), + "bbox": region["bbox"], + "center": region["center"], + "text_color": text_color, + "text_color_hex": self.color_extractor.rgb_to_hex(text_color), + "bg_color": bg_color, + "bg_color_hex": self.color_extractor.rgb_to_hex(bg_color), + "contrast_ratio": analysis["contrast_ratio"], + "wcag_aa": analysis["wcag_aa"], + "wcag_aaa": analysis["wcag_aaa"], + "level": analysis["level"], } results.append(result) return results - def generate_report( - self, - results: List[Dict], - output_format: str = 'json' - ) -> str: + def generate_report(self, results: List[Dict], output_format: str = "json") -> str: """ Generate a report from analysis results. @@ -132,10 +115,10 @@ def generate_report( Returns: Formatted report string """ - if output_format == 'json': + if output_format == "json": return json.dumps(results, indent=2, ensure_ascii=False) - elif output_format == 'text': + elif output_format == "text": report_lines = [] report_lines.append("=" * 80) report_lines.append("CONTRAST ANALYSIS REPORT") @@ -154,24 +137,30 @@ def generate_report( f" Background Color: RGB{result['bg_color']} " f"({result['bg_color_hex']})" ) + report_lines.append(f" Contrast Ratio: {result['contrast_ratio']}:1") + report_lines.append( + f" WCAG AA: {'✓ PASS' if result['wcag_aa'] else '✗ FAIL'}" + ) report_lines.append( - f" Contrast Ratio: {result['contrast_ratio']}:1" + f" WCAG AAA: {'✓ PASS' if result['wcag_aaa'] else '✗ FAIL'}" ) - report_lines.append(f" WCAG AA: {'✓ PASS' if result['wcag_aa'] else '✗ FAIL'}") - report_lines.append(f" WCAG AAA: {'✓ PASS' if result['wcag_aaa'] else '✗ FAIL'}") report_lines.append(f" Level: {result['level']}") report_lines.append("") # Summary total = len(results) - aa_pass = sum(1 for r in results if r['wcag_aa']) - aaa_pass = sum(1 for r in results if r['wcag_aaa']) + aa_pass = sum(1 for r in results if r["wcag_aa"]) + aaa_pass = sum(1 for r in results if r["wcag_aaa"]) report_lines.append("=" * 80) report_lines.append("SUMMARY") report_lines.append("=" * 80) - report_lines.append(f"WCAG AA Compliance: {aa_pass}/{total} ({aa_pass/total*100:.1f}%)") - report_lines.append(f"WCAG AAA Compliance: {aaa_pass}/{total} ({aaa_pass/total*100:.1f}%)") + report_lines.append( + f"WCAG AA Compliance: {aa_pass}/{total} ({aa_pass/total*100:.1f}%)" + ) + report_lines.append( + f"WCAG AAA Compliance: {aaa_pass}/{total} ({aaa_pass/total*100:.1f}%)" + ) report_lines.append("=" * 80) return "\n".join(report_lines) @@ -185,40 +174,28 @@ def main(): Command-line interface for ContrastCheck. """ parser = argparse.ArgumentParser( - description='Analyze text-background contrast ratios in UI screenshots' - ) - parser.add_argument( - 'image', - type=str, - help='Path to the UI screenshot image' + description="Analyze text-background contrast ratios in UI screenshots" ) + parser.add_argument("image", type=str, help="Path to the UI screenshot image") parser.add_argument( - '-o', '--output', - type=str, - help='Output file path for the report' + "-o", "--output", type=str, help="Output file path for the report" ) parser.add_argument( - '-f', '--format', + "-f", + "--format", type=str, - choices=['json', 'text'], - default='text', - help='Output format (default: text)' - ) - parser.add_argument( - '--large-text', - action='store_true', - help='Treat all text as large text (18pt+ or 14pt+ bold)' + choices=["json", "text"], + default="text", + help="Output format (default: text)", ) parser.add_argument( - '--gpu', - action='store_true', - help='Use GPU for OCR processing' + "--large-text", + action="store_true", + help="Treat all text as large text (18pt+ or 14pt+ bold)", ) + parser.add_argument("--gpu", action="store_true", help="Use GPU for OCR processing") parser.add_argument( - '--lang', - type=str, - default='en', - help='Language for OCR (default: en)' + "--lang", type=str, default="en", help="Language for OCR (default: en)" ) args = parser.parse_args() @@ -245,12 +222,12 @@ def main(): # Output report if args.output: - with open(args.output, 'w', encoding='utf-8') as f: + with open(args.output, "w", encoding="utf-8") as f: f.write(report) print(f"\nReport saved to: {args.output}") else: print("\n" + report) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/contrast_check/ocr_extractor.py b/contrast_check/ocr_extractor.py index 55e3208..0a0dcc6 100644 --- a/contrast_check/ocr_extractor.py +++ b/contrast_check/ocr_extractor.py @@ -2,10 +2,10 @@ OCR text extraction module using PaddleOCR. """ -import numpy as np -from typing import List, Tuple, Dict, Any -from PIL import Image +from typing import Any, Dict, List, Tuple + import cv2 +import numpy as np class OCRExtractor: @@ -13,7 +13,7 @@ class OCRExtractor: Extract text and coordinates from images using PaddleOCR. """ - def __init__(self, use_gpu: bool = False, lang: str = 'en'): + def __init__(self, use_gpu: bool = False, lang: str = "en"): """ Initialize OCR extractor. @@ -30,16 +30,10 @@ def __init__(self, use_gpu: bool = False, lang: str = 'en'): ) self.ocr = PaddleOCR( - use_angle_cls=True, - lang=lang, - use_gpu=use_gpu, - show_log=False + use_angle_cls=True, lang=lang, use_gpu=use_gpu, show_log=False ) - def extract_text_regions( - self, - image_path: str - ) -> List[Dict[str, Any]]: + def extract_text_regions(self, image_path: str) -> List[Dict[str, Any]]: """ Extract text regions from an image. @@ -76,19 +70,19 @@ def extract_text_regions( center_x = int(np.mean(bbox_array[:, 0])) center_y = int(np.mean(bbox_array[:, 1])) - text_regions.append({ - 'text': text, - 'confidence': confidence, - 'bbox': bbox, - 'center': (center_x, center_y) - }) + text_regions.append( + { + "text": text, + "confidence": confidence, + "bbox": bbox, + "center": (center_x, center_y), + } + ) return text_regions def get_text_region_mask( - self, - image_shape: Tuple[int, int, int], - bbox: List[List[float]] + self, image_shape: Tuple[int, int, int], bbox: List[List[float]] ) -> np.ndarray: """ Create a binary mask for a text region. @@ -100,7 +94,7 @@ def get_text_region_mask( Returns: Binary mask with text region marked as True """ - mask = np.zeros(image_shape[:2], dtype=bool) + mask = np.zeros(image_shape[:2], dtype=np.uint8) points = np.array(bbox, dtype=np.int32) - cv2.fillPoly(mask, [points], True) - return mask + cv2.fillPoly(mask, [points], 1) + return mask.astype(bool) diff --git a/pyproject.toml b/pyproject.toml index 3f64844..a83e9bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "contrastcheck" version = "0.1.0" description = "A tool for analyzing text-background contrast ratios in UI screenshots" readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.10" license = {text = "MIT"} authors = [ {name = "ContrastCheck Contributors"} @@ -20,10 +20,10 @@ classifiers = [ "Topic :: Multimedia :: Graphics", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] dependencies = [ @@ -54,14 +54,14 @@ docs = [ contrastcheck = "contrast_check.main:main" [project.urls] -Homepage = "https://github.com/yourusername/ContrastCheck" -Documentation = "https://github.com/yourusername/ContrastCheck#readme" -Repository = "https://github.com/yourusername/ContrastCheck" -"Bug Tracker" = "https://github.com/yourusername/ContrastCheck/issues" +Homepage = "https://github.com/longway-code/ContrastCheck" +Documentation = "https://github.com/longway-code/ContrastCheck#readme" +Repository = "https://github.com/longway-code/ContrastCheck" +"Bug Tracker" = "https://github.com/longway-code/ContrastCheck/issues" [tool.black] line-length = 88 -target-version = ['py38', 'py39', 'py310', 'py311'] +target-version = ['py310', 'py311', 'py312', 'py313'] include = '\.pyi?$' extend-exclude = ''' /( @@ -87,7 +87,7 @@ use_parentheses = true ensure_newline_before_comments = true [tool.mypy] -python_version = "3.8" +python_version = "3.10" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = false diff --git a/setup.py b/setup.py index a223319..a8c42a9 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ description='A tool for analyzing text-background contrast ratios in UI screenshots', long_description=long_description, long_description_content_type='text/markdown', - url='https://github.com/yourusername/ContrastCheck', + url='https://github.com/longway-code/ContrastCheck', packages=find_packages(exclude=['tests', 'tests.*', 'examples']), classifiers=[ 'Development Status :: 3 - Alpha', @@ -25,12 +25,12 @@ 'Topic :: Multimedia :: Graphics', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ], - python_requires='>=3.8', + python_requires='>=3.10', install_requires=[ 'paddleocr>=2.7.0', 'paddlepaddle>=2.5.0', diff --git a/tests/test_color_extractor.py b/tests/test_color_extractor.py index 64f8cc1..1228d04 100644 --- a/tests/test_color_extractor.py +++ b/tests/test_color_extractor.py @@ -3,7 +3,9 @@ """ import unittest + import numpy as np + from contrast_check.color_extractor import ColorExtractor @@ -68,10 +70,7 @@ def test_extract_background_color_with_margin(self): bbox = [[45, 45], [55, 45], [55, 55], [45, 55]] color = self.extractor.extract_background_color( - image, - text_mask, - bbox, - margin=10 + image, text_mask, bbox, margin=10 ) # Should extract blue color (0, 0, 255) in RGB @@ -81,19 +80,19 @@ def test_rgb_to_hex_black(self): """Test RGB to hex conversion for black.""" rgb = (0, 0, 0) hex_color = self.extractor.rgb_to_hex(rgb) - self.assertEqual(hex_color, '#000000') + self.assertEqual(hex_color, "#000000") def test_rgb_to_hex_white(self): """Test RGB to hex conversion for white.""" rgb = (255, 255, 255) hex_color = self.extractor.rgb_to_hex(rgb) - self.assertEqual(hex_color, '#ffffff') + self.assertEqual(hex_color, "#ffffff") def test_rgb_to_hex_custom_color(self): """Test RGB to hex conversion for custom color.""" rgb = (128, 64, 192) hex_color = self.extractor.rgb_to_hex(rgb) - self.assertEqual(hex_color, '#8040c0') + self.assertEqual(hex_color, "#8040c0") def test_extract_dominant_color_from_mixed(self): """Test extraction of dominant color from mixed pixels.""" @@ -109,8 +108,8 @@ def test_extract_dominant_color_from_mixed(self): # Should extract red as dominant color (255, 0, 0) in RGB # Allow some tolerance due to clustering self.assertTrue(color[0] > 200) # High red channel - self.assertTrue(color[1] < 50) # Low green channel - self.assertTrue(color[2] < 50) # Low blue channel + self.assertTrue(color[1] < 50) # Low green channel + self.assertTrue(color[2] < 50) # Low blue channel def test_color_extraction_boundary_cases(self): """Test color extraction at image boundaries.""" @@ -128,5 +127,5 @@ def test_color_extraction_boundary_cases(self): self.assertEqual(len(color), 3) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_contrast_checker.py b/tests/test_contrast_checker.py index 53a998f..fed7fd9 100644 --- a/tests/test_contrast_checker.py +++ b/tests/test_contrast_checker.py @@ -3,6 +3,7 @@ """ import unittest + from contrast_check.contrast_checker import ContrastChecker @@ -52,8 +53,8 @@ def test_wcag_compliance_aa_normal_pass(self): white = (255, 255, 255) ratio = self.checker.calculate_contrast_ratio(black, white) compliance = self.checker.check_wcag_compliance(ratio, is_large_text=False) - self.assertTrue(compliance['AA']) - self.assertTrue(compliance['AAA']) + self.assertTrue(compliance["AA"]) + self.assertTrue(compliance["AAA"]) def test_wcag_compliance_aa_normal_fail(self): """Test WCAG AA compliance for normal text (failing).""" @@ -62,8 +63,8 @@ def test_wcag_compliance_aa_normal_fail(self): white = (255, 255, 255) ratio = self.checker.calculate_contrast_ratio(light_gray, white) compliance = self.checker.check_wcag_compliance(ratio, is_large_text=False) - self.assertFalse(compliance['AA']) - self.assertFalse(compliance['AAA']) + self.assertFalse(compliance["AA"]) + self.assertFalse(compliance["AAA"]) def test_wcag_compliance_large_text(self): """Test WCAG compliance for large text.""" @@ -72,11 +73,6 @@ def test_wcag_compliance_large_text(self): color2 = (255, 255, 255) ratio = self.checker.calculate_contrast_ratio(color1, color2) - # Should pass for large text (3:1 threshold) - compliance_large = self.checker.check_wcag_compliance(ratio, is_large_text=True) - # May or may not pass for normal text depending on exact ratio - compliance_normal = self.checker.check_wcag_compliance(ratio, is_large_text=False) - # The ratio should be around 3.9:1, which passes large text AA self.assertGreater(ratio, 3.0) @@ -106,12 +102,12 @@ def test_analyze_contrast_complete(self): bg_color = (255, 255, 255) analysis = self.checker.analyze_contrast(text_color, bg_color) - self.assertEqual(analysis['text_color'], text_color) - self.assertEqual(analysis['bg_color'], bg_color) - self.assertAlmostEqual(analysis['contrast_ratio'], 21.0, places=1) - self.assertTrue(analysis['wcag_aa']) - self.assertTrue(analysis['wcag_aaa']) - self.assertIn('Excellent', analysis['level']) + self.assertEqual(analysis["text_color"], text_color) + self.assertEqual(analysis["bg_color"], bg_color) + self.assertAlmostEqual(analysis["contrast_ratio"], 21.0, places=1) + self.assertTrue(analysis["wcag_aa"]) + self.assertTrue(analysis["wcag_aaa"]) + self.assertIn("Excellent", analysis["level"]) def test_known_contrast_values(self): """Test against known WCAG contrast values.""" @@ -122,5 +118,5 @@ def test_known_contrast_values(self): self.assertAlmostEqual(ratio, 7.0, delta=0.5) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_main.py b/tests/test_main.py index 81b535b..46f17ec 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -2,38 +2,36 @@ Unit tests for main application module. """ -import unittest import json -from unittest.mock import Mock, patch, MagicMock +import unittest +from unittest.mock import Mock, patch + from contrast_check.main import ContrastAnalyzer class TestContrastAnalyzer(unittest.TestCase): """Test cases for ContrastAnalyzer class.""" - @patch('contrast_check.main.OCRExtractor') - @patch('contrast_check.main.ColorExtractor') - @patch('contrast_check.main.ContrastChecker') + @patch("contrast_check.main.OCRExtractor") + @patch("contrast_check.main.ColorExtractor") + @patch("contrast_check.main.ContrastChecker") def test_initialization(self, mock_checker, mock_color, mock_ocr): """Test ContrastAnalyzer initialization.""" - analyzer = ContrastAnalyzer( - use_gpu=True, - lang='ch', - n_text_colors=5, - n_bg_colors=5 - ) + ContrastAnalyzer(use_gpu=True, lang="ch", n_text_colors=5, n_bg_colors=5) # Verify OCRExtractor was initialized correctly - mock_ocr.assert_called_once_with(use_gpu=True, lang='ch') + mock_ocr.assert_called_once_with(use_gpu=True, lang="ch") # Verify ColorExtractor was initialized correctly mock_color.assert_called_once_with(n_text_colors=5, n_bg_colors=5) - @patch('contrast_check.main.cv2.imread') - @patch('contrast_check.main.OCRExtractor') - @patch('contrast_check.main.ColorExtractor') - @patch('contrast_check.main.ContrastChecker') - def test_analyze_image_no_text(self, mock_checker, mock_color, mock_ocr, mock_imread): + @patch("contrast_check.main.cv2.imread") + @patch("contrast_check.main.OCRExtractor") + @patch("contrast_check.main.ColorExtractor") + @patch("contrast_check.main.ContrastChecker") + def test_analyze_image_no_text( + self, mock_checker, mock_color, mock_ocr, mock_imread + ): """Test image analysis with no text detected.""" # Mock OCR to return empty list mock_ocr_instance = Mock() @@ -47,15 +45,17 @@ def test_analyze_image_no_text(self, mock_checker, mock_color, mock_ocr, mock_im mock_checker.return_value = mock_checker_instance analyzer = ContrastAnalyzer() - results = analyzer.analyze_image('test_image.jpg') + results = analyzer.analyze_image("test_image.jpg") self.assertEqual(results, []) - @patch('contrast_check.main.cv2.imread') - @patch('contrast_check.main.OCRExtractor') - @patch('contrast_check.main.ColorExtractor') - @patch('contrast_check.main.ContrastChecker') - def test_analyze_image_with_text(self, mock_checker, mock_color, mock_ocr, mock_imread): + @patch("contrast_check.main.cv2.imread") + @patch("contrast_check.main.OCRExtractor") + @patch("contrast_check.main.ColorExtractor") + @patch("contrast_check.main.ContrastChecker") + def test_analyze_image_with_text( + self, mock_checker, mock_color, mock_ocr, mock_imread + ): """Test image analysis with text detected.""" import numpy as np @@ -66,162 +66,174 @@ def test_analyze_image_with_text(self, mock_checker, mock_color, mock_ocr, mock_ # Mock OCR results mock_text_regions = [ { - 'text': 'Hello', - 'confidence': 0.95, - 'bbox': [[10, 10], [50, 10], [50, 30], [10, 30]], - 'center': (30, 20) + "text": "Hello", + "confidence": 0.95, + "bbox": [[10, 10], [50, 10], [50, 30], [10, 30]], + "center": (30, 20), } ] mock_ocr_instance = Mock() mock_ocr_instance.extract_text_regions.return_value = mock_text_regions - mock_ocr_instance.get_text_region_mask.return_value = np.ones((100, 100), dtype=bool) + mock_ocr_instance.get_text_region_mask.return_value = np.ones( + (100, 100), dtype=bool + ) mock_ocr.return_value = mock_ocr_instance # Mock color extraction mock_color_instance = Mock() mock_color_instance.extract_text_color.return_value = (0, 0, 0) mock_color_instance.extract_background_color.return_value = (255, 255, 255) - mock_color_instance.rgb_to_hex.side_effect = lambda rgb: '#{:02x}{:02x}{:02x}'.format(*rgb) + mock_color_instance.rgb_to_hex.side_effect = ( + lambda rgb: "#{:02x}{:02x}{:02x}".format(*rgb) + ) mock_color.return_value = mock_color_instance # Mock contrast checker mock_checker_instance = Mock() mock_checker_instance.analyze_contrast.return_value = { - 'text_color': (0, 0, 0), - 'bg_color': (255, 255, 255), - 'contrast_ratio': 21.0, - 'wcag_aa': True, - 'wcag_aaa': True, - 'level': 'Excellent (AAA)', - 'is_large_text': False + "text_color": (0, 0, 0), + "bg_color": (255, 255, 255), + "contrast_ratio": 21.0, + "wcag_aa": True, + "wcag_aaa": True, + "level": "Excellent (AAA)", + "is_large_text": False, } mock_checker.return_value = mock_checker_instance analyzer = ContrastAnalyzer() - results = analyzer.analyze_image('test_image.jpg') + results = analyzer.analyze_image("test_image.jpg") # Verify results self.assertEqual(len(results), 1) - self.assertEqual(results[0]['text'], 'Hello') - self.assertAlmostEqual(results[0]['confidence'], 0.95) - self.assertEqual(results[0]['contrast_ratio'], 21.0) - self.assertTrue(results[0]['wcag_aa']) - self.assertTrue(results[0]['wcag_aaa']) + self.assertEqual(results[0]["text"], "Hello") + self.assertAlmostEqual(results[0]["confidence"], 0.95) + self.assertEqual(results[0]["contrast_ratio"], 21.0) + self.assertTrue(results[0]["wcag_aa"]) + self.assertTrue(results[0]["wcag_aaa"]) def test_generate_report_json(self): """Test JSON report generation.""" - with patch('contrast_check.main.OCRExtractor'), \ - patch('contrast_check.main.ColorExtractor'), \ - patch('contrast_check.main.ContrastChecker'): + with ( + patch("contrast_check.main.OCRExtractor"), + patch("contrast_check.main.ColorExtractor"), + patch("contrast_check.main.ContrastChecker"), + ): analyzer = ContrastAnalyzer() mock_results = [ { - 'index': 0, - 'text': 'Test', - 'confidence': 0.95, - 'contrast_ratio': 7.5, - 'wcag_aa': True, - 'wcag_aaa': True + "index": 0, + "text": "Test", + "confidence": 0.95, + "contrast_ratio": 7.5, + "wcag_aa": True, + "wcag_aaa": True, } ] - report = analyzer.generate_report(mock_results, output_format='json') + report = analyzer.generate_report(mock_results, output_format="json") # Should be valid JSON parsed = json.loads(report) self.assertEqual(len(parsed), 1) - self.assertEqual(parsed[0]['text'], 'Test') + self.assertEqual(parsed[0]["text"], "Test") def test_generate_report_text(self): """Test text report generation.""" - with patch('contrast_check.main.OCRExtractor'), \ - patch('contrast_check.main.ColorExtractor'), \ - patch('contrast_check.main.ContrastChecker'): + with ( + patch("contrast_check.main.OCRExtractor"), + patch("contrast_check.main.ColorExtractor"), + patch("contrast_check.main.ContrastChecker"), + ): analyzer = ContrastAnalyzer() mock_results = [ { - 'index': 0, - 'text': 'Test', - 'confidence': 0.95, - 'text_color': (0, 0, 0), - 'text_color_hex': '#000000', - 'bg_color': (255, 255, 255), - 'bg_color_hex': '#ffffff', - 'contrast_ratio': 21.0, - 'wcag_aa': True, - 'wcag_aaa': True, - 'level': 'Excellent (AAA)' + "index": 0, + "text": "Test", + "confidence": 0.95, + "text_color": (0, 0, 0), + "text_color_hex": "#000000", + "bg_color": (255, 255, 255), + "bg_color_hex": "#ffffff", + "contrast_ratio": 21.0, + "wcag_aa": True, + "wcag_aaa": True, + "level": "Excellent (AAA)", } ] - report = analyzer.generate_report(mock_results, output_format='text') + report = analyzer.generate_report(mock_results, output_format="text") # Check report contains expected sections - self.assertIn('CONTRAST ANALYSIS REPORT', report) - self.assertIn('Test', report) - self.assertIn('21.0:1', report) - self.assertIn('SUMMARY', report) - self.assertIn('WCAG AA Compliance', report) + self.assertIn("CONTRAST ANALYSIS REPORT", report) + self.assertIn("Test", report) + self.assertIn("21.0:1", report) + self.assertIn("SUMMARY", report) + self.assertIn("WCAG AA Compliance", report) def test_generate_report_invalid_format(self): """Test report generation with invalid format.""" - with patch('contrast_check.main.OCRExtractor'), \ - patch('contrast_check.main.ColorExtractor'), \ - patch('contrast_check.main.ContrastChecker'): + with ( + patch("contrast_check.main.OCRExtractor"), + patch("contrast_check.main.ColorExtractor"), + patch("contrast_check.main.ContrastChecker"), + ): analyzer = ContrastAnalyzer() with self.assertRaises(ValueError): - analyzer.generate_report([], output_format='invalid') + analyzer.generate_report([], output_format="invalid") def test_generate_report_summary_statistics(self): """Test that summary statistics are calculated correctly.""" - with patch('contrast_check.main.OCRExtractor'), \ - patch('contrast_check.main.ColorExtractor'), \ - patch('contrast_check.main.ContrastChecker'): + with ( + patch("contrast_check.main.OCRExtractor"), + patch("contrast_check.main.ColorExtractor"), + patch("contrast_check.main.ContrastChecker"), + ): analyzer = ContrastAnalyzer() mock_results = [ { - 'index': 0, - 'text': 'Good', - 'confidence': 0.95, - 'text_color': (0, 0, 0), - 'text_color_hex': '#000000', - 'bg_color': (255, 255, 255), - 'bg_color_hex': '#ffffff', - 'contrast_ratio': 21.0, - 'wcag_aa': True, - 'wcag_aaa': True, - 'level': 'Excellent (AAA)' + "index": 0, + "text": "Good", + "confidence": 0.95, + "text_color": (0, 0, 0), + "text_color_hex": "#000000", + "bg_color": (255, 255, 255), + "bg_color_hex": "#ffffff", + "contrast_ratio": 21.0, + "wcag_aa": True, + "wcag_aaa": True, + "level": "Excellent (AAA)", }, { - 'index': 1, - 'text': 'Poor', - 'confidence': 0.90, - 'text_color': (200, 200, 200), - 'text_color_hex': '#c8c8c8', - 'bg_color': (255, 255, 255), - 'bg_color_hex': '#ffffff', - 'contrast_ratio': 1.5, - 'wcag_aa': False, - 'wcag_aaa': False, - 'level': 'Poor (Fails WCAG)' - } + "index": 1, + "text": "Poor", + "confidence": 0.90, + "text_color": (200, 200, 200), + "text_color_hex": "#c8c8c8", + "bg_color": (255, 255, 255), + "bg_color_hex": "#ffffff", + "contrast_ratio": 1.5, + "wcag_aa": False, + "wcag_aaa": False, + "level": "Poor (Fails WCAG)", + }, ] - report = analyzer.generate_report(mock_results, output_format='text') + report = analyzer.generate_report(mock_results, output_format="text") # Check that summary shows 50% AA compliance (1 out of 2) - self.assertIn('1/2', report) - self.assertIn('50.0%', report) + self.assertIn("1/2", report) + self.assertIn("50.0%", report) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_ocr_extractor.py b/tests/test_ocr_extractor.py index ac2f57c..df2cafb 100644 --- a/tests/test_ocr_extractor.py +++ b/tests/test_ocr_extractor.py @@ -3,43 +3,39 @@ """ import unittest +from unittest.mock import Mock, patch + import numpy as np -from unittest.mock import Mock, patch, MagicMock + from contrast_check.ocr_extractor import OCRExtractor class TestOCRExtractor(unittest.TestCase): """Test cases for OCRExtractor class.""" - @patch('contrast_check.ocr_extractor.PaddleOCR') + @patch("paddleocr.PaddleOCR") def test_initialization(self, mock_paddle): """Test OCRExtractor initialization.""" - extractor = OCRExtractor(use_gpu=False, lang='en') + OCRExtractor(use_gpu=False, lang="en") # Check that PaddleOCR was called with correct parameters mock_paddle.assert_called_once_with( - use_angle_cls=True, - lang='en', - use_gpu=False, - show_log=False + use_angle_cls=True, lang="en", use_gpu=False, show_log=False ) - @patch('contrast_check.ocr_extractor.PaddleOCR') + @patch("paddleocr.PaddleOCR") def test_initialization_with_gpu(self, mock_paddle): """Test OCRExtractor initialization with GPU.""" - extractor = OCRExtractor(use_gpu=True, lang='ch') + OCRExtractor(use_gpu=True, lang="ch") mock_paddle.assert_called_once_with( - use_angle_cls=True, - lang='ch', - use_gpu=True, - show_log=False + use_angle_cls=True, lang="ch", use_gpu=True, show_log=False ) def test_get_text_region_mask(self): """Test text region mask creation.""" # Create a dummy extractor (without actual PaddleOCR initialization) - with patch('contrast_check.ocr_extractor.PaddleOCR'): + with patch("paddleocr.PaddleOCR"): extractor = OCRExtractor() # Test with a simple rectangular bbox @@ -62,7 +58,7 @@ def test_get_text_region_mask(self): def test_get_text_region_mask_complex_shape(self): """Test mask creation with a complex polygon.""" - with patch('contrast_check.ocr_extractor.PaddleOCR'): + with patch("paddleocr.PaddleOCR"): extractor = OCRExtractor() image_shape = (200, 200, 3) @@ -74,8 +70,8 @@ def test_get_text_region_mask_complex_shape(self): self.assertEqual(mask.shape, (200, 200)) self.assertTrue(np.any(mask)) - @patch('contrast_check.ocr_extractor.cv2.imread') - @patch('contrast_check.ocr_extractor.PaddleOCR') + @patch("contrast_check.ocr_extractor.cv2.imread") + @patch("paddleocr.PaddleOCR") def test_extract_text_regions_empty_result(self, mock_paddle, mock_imread): """Test extraction with no text detected.""" # Mock OCR to return empty result @@ -87,24 +83,18 @@ def test_extract_text_regions_empty_result(self, mock_paddle, mock_imread): mock_imread.return_value = np.zeros((100, 100, 3), dtype=np.uint8) extractor = OCRExtractor() - results = extractor.extract_text_regions('dummy_path.jpg') + results = extractor.extract_text_regions("dummy_path.jpg") self.assertEqual(results, []) - @patch('contrast_check.ocr_extractor.cv2.imread') - @patch('contrast_check.ocr_extractor.PaddleOCR') + @patch("contrast_check.ocr_extractor.cv2.imread") + @patch("paddleocr.PaddleOCR") def test_extract_text_regions_with_data(self, mock_paddle, mock_imread): """Test extraction with mock OCR data.""" # Mock OCR result format: [bbox, (text, confidence)] mock_ocr_result = [ - [ - [[10, 10], [50, 10], [50, 30], [10, 30]], - ('Hello', 0.95) - ], - [ - [[60, 20], [100, 20], [100, 40], [60, 40]], - ('World', 0.92) - ] + [[[10, 10], [50, 10], [50, 30], [10, 30]], ("Hello", 0.95)], + [[[60, 20], [100, 20], [100, 40], [60, 40]], ("World", 0.92)], ] mock_ocr_instance = Mock() @@ -115,23 +105,23 @@ def test_extract_text_regions_with_data(self, mock_paddle, mock_imread): mock_imread.return_value = np.zeros((100, 150, 3), dtype=np.uint8) extractor = OCRExtractor() - results = extractor.extract_text_regions('dummy_path.jpg') + results = extractor.extract_text_regions("dummy_path.jpg") # Check results self.assertEqual(len(results), 2) # Check first result - self.assertEqual(results[0]['text'], 'Hello') - self.assertAlmostEqual(results[0]['confidence'], 0.95) - self.assertEqual(len(results[0]['bbox']), 4) - self.assertIsInstance(results[0]['center'], tuple) + self.assertEqual(results[0]["text"], "Hello") + self.assertAlmostEqual(results[0]["confidence"], 0.95) + self.assertEqual(len(results[0]["bbox"]), 4) + self.assertIsInstance(results[0]["center"], tuple) # Check second result - self.assertEqual(results[1]['text'], 'World') - self.assertAlmostEqual(results[1]['confidence'], 0.92) + self.assertEqual(results[1]["text"], "World") + self.assertAlmostEqual(results[1]["confidence"], 0.92) - @patch('contrast_check.ocr_extractor.cv2.imread') - @patch('contrast_check.ocr_extractor.PaddleOCR') + @patch("contrast_check.ocr_extractor.cv2.imread") + @patch("paddleocr.PaddleOCR") def test_extract_text_regions_invalid_image(self, mock_paddle, mock_imread): """Test extraction with invalid image path.""" mock_ocr_instance = Mock() @@ -143,18 +133,13 @@ def test_extract_text_regions_invalid_image(self, mock_paddle, mock_imread): extractor = OCRExtractor() with self.assertRaises(ValueError): - extractor.extract_text_regions('invalid_path.jpg') + extractor.extract_text_regions("invalid_path.jpg") - @patch('contrast_check.ocr_extractor.cv2.imread') - @patch('contrast_check.ocr_extractor.PaddleOCR') + @patch("contrast_check.ocr_extractor.cv2.imread") + @patch("paddleocr.PaddleOCR") def test_center_calculation(self, mock_paddle, mock_imread): """Test that center point is calculated correctly.""" - mock_ocr_result = [ - [ - [[0, 0], [100, 0], [100, 50], [0, 50]], - ('Test', 0.99) - ] - ] + mock_ocr_result = [[[[0, 0], [100, 0], [100, 50], [0, 50]], ("Test", 0.99)]] mock_ocr_instance = Mock() mock_ocr_instance.ocr.return_value = [mock_ocr_result] @@ -163,11 +148,11 @@ def test_center_calculation(self, mock_paddle, mock_imread): mock_imread.return_value = np.zeros((100, 150, 3), dtype=np.uint8) extractor = OCRExtractor() - results = extractor.extract_text_regions('dummy_path.jpg') + results = extractor.extract_text_regions("dummy_path.jpg") # Center should be at (50, 25) - self.assertEqual(results[0]['center'], (50, 25)) + self.assertEqual(results[0]["center"], (50, 25)) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main()